I / O, fichiers

(I/O = Input / Output)

Préliminaires : lien symbolique (raccourci) ; chemin absolu / relatif.

Java I / O classique

API présente dès le début de java, package java.io.
La classe principale est java.io.File, qui représente à la fois un fichier et ou un répertoire.

Exemple d'utilisation :
on suppose que le répertoire home contient un fichier app.conf
Créer un répertoire .configdir et déplacer app.conf dedans.
=> faire en java ce qui se ferait en bash de cette manière :
cd ~
mkdir .configdir
mv app.conf .configdir
En java :
File homedir = new File(System.getProperty("user.home"));
File f = new File(homedir, "app.conf");
if (f.exists() && f.isFile() && f.canRead()) {
    File configdir = new File(f, ".configdir");
    configdir.mkdir();
    f.renameTo(new File(configdir, ".config"));
}
java.io.File contient un grand nombre de méthodes :
File f = new File("/path/to/file");

// Gestion des droits
boolean canR = f.canRead();
boolean canW = f.canWrite();
boolean canX = f.canExecute();
boolean ok;
ok = f.setReadOnly();
ok = f.setReadable(true);
ok = f.setWritable(false);
ok = f.setExecutable(true);

// Différents aspects du nom du fichier
File absF = f.getAbsoluteFile();
File canF = f.getCanonicalFile();
String absName = f.getAbsolutePath();
String canName = f.getCanonicalPath();
String name = f.getName();
String pName = f.getParent();
URI fileURI = f.toURI(); // Create URI for File path

// Metadata
boolean exists = f.exists();
boolean isAbs = f.isAbsolute();
boolean isDir = f.isDirectory();
boolean isFile = f.isFile();
boolean isHidden = f.isHidden();
long modTime = f.lastModified(); // milliseconds since epoch
boolean updateOK = f.setLastModified(updateTime); // milliseconds
long fileLen = f.length();

// Opérations sur le fichier
boolean renamed = f.renameTo(new File("/new/filename"));
boolean deleted = f.delete();
boolean createdOK = f.createNewFile(); // n'écrase pas le fichier existant

// Gestion de fichier temporaire
File tmp = File.createTempFile("my-tmp", ".tmp");
tmp.deleteOnExit();

// Gestion de répertoire
File dir = new File("/path/to/dir");
boolean createdDir = dir.mkdir();
String[] fileNames = dir.list();
File[] files = dir.listFiles();
(voir FileMethodsExamples.java)

Noter qu'il n'y a pas de méthode pour lire le contenu d'un fichier.

Lecture écriture

L'API java pour lire ou écrire dans des fichiers est déroutante au premier abord mais pratique à l'utilisation.
Elle comporte 4 classes de base, permattant de travailler soit sur des octets, soit sur des caractères :
LectureEcriture
Caractère
(texte)
Reader Writer
Octet
(binaire)
InputStream OutputStream
Chaque classe a des sous-classes permettant de travailler sur des sources de données variées (fichier local, fichier distant, chaîne de caractères, tableau de caractères...) et fournissant diverses fonctionnalités.

On choisit la source de données et les fonctionnalités utilisables en emboîtant ces classes, qui implémentnent le pattern Decorator.

Streams

Les I/O streams (attention : différents des streams introduits en java8 pour les collections) permettent de gérer des flux d'octets en provenance du disque ou d'autres sources.
API organisée autour de 2 classes abstraites : InputStream et OutputStream et de leurs sous-classes.

System.in et System.out sont des instances de ces classes.

Exemple : compte le nombre de "a" dans un fichier (code ASCII 97)
    public static void main(String[] args){
        try (InputStream is = new FileInputStream("test.txt")) {
            byte[] buf = new byte[4096];
            int len, count = 0;
            while ((len = is.read(buf)) > 0) {
                for (int i=0; i<len; i++)
                    if (buf[i] == 97) count++;
            }
            System.out.println("Trouvé "+ count + " 'a'");
        }
        catch (IOException e) {
            e.printStackTrace();
        }
    }
(voir InputStreamExample.java)

Noter la syntaxe inhabituelle du try, voir try-with-resources plus loin.

Readers et Writers

Travailler avec des octets n'étant pas toujours pratique, deux autres classes abstraites, Reader et Writer permettent de travailler au niveau des caractères.
Sous-classes fréquemment utilisées :
FileReader
BufferedReader
InputStreamReader
FileWriter
PrintWriter
BufferedWriter
try (BufferedReader in = new BufferedReader(new FileReader("test.txt"))) {
    String line;
    while((line = in.readLine()) != null) {
        System.out.println(line);
    }
} catch (IOException e) { }
(code dans BufferedReaderExample.java)

Pour comprendre l'intérêt de BufferedReader, voir BufferedReaderCompare.java (BufferedReader est bien plus rapide que FileReader).

On peut passer d'une représentation "octet" à une représentation "caractère" en utilisant les classes InputStreamReader et OutputStreamWriter.

Fichiers distants

En utilisant des classes du package java.net, on peut lire des fichiers distants :
import java.io.*;
import java.net.*;

class RemoteExample{
    
    public static void main(String[] args){
        URL url = null;
        try{
            url = new URL("https://larzac.info/cnam/index.html");
        }
        catch(MalformedURLException e){
            e.printStackTrace();
        }
        try(
            BufferedReader reader = new BufferedReader(new InputStreamReader(url.openStream()));
        ){
            String line;
            while((line = reader.readLine()) != null){
                System.out.println(line);
            }
        }
        catch(IOException e){
            e.printStackTrace();
        }
    }
}
(code dans RemoteExample.java)

try with resources (TWR)

A lire avant : Exceptions

Mécanisme introduit en java 7, particulièrement utile pour java.io :
Lorsqu'on fait un try "normal", si on utilise des resources qui doivent être libérées, les clauses catch et finally sont souvent fastidieuses à écrire.
Cette nouvelle syntaxe permet de passer en paramètre au bloc try des objets qui ont éventuellement besoin de nettoyage.
javac s'occupe alors d'insérer le code nécessaire pour libérer les resources, qu'on n'a donc plus besoin d'écrire.

A UTILISER CHAQUE FOIS QUE C'EST POSSIBLE, c'est à dire pour construire des objets de classes implémentant java.lang.AutoCloseable, qui déclare un seule méthode, close().

TWR permet de passer en paramètre de try la création de plusieurs objets :
try (
    BufferedReader in = new BufferedReader(new FileReader("profile"));
    PrintWriter out = new PrintWriter(new BufferedWriter(new FileWriter("profile.bak")))
) {
    String line;
    while((line = in.readLine()) != null) {
        out.println(line);
    }
} catch (IOException e) {
    // FileNotFoundException, etc.
}
Similaire à using en C#.

Java I / O moderne (java.nio)

Nouvelle API introduite en java 4, fournit un remplacement presque complet de java.io ; plus simple d'utilisation.
Les sous-classes de Reader et Writer sont toujours utilisées.

Les classes principales sont dans le package java.nio.file.

Paths

Représente le chemin d'un fichier dans un système de fichier.
Peut être absolu ou relatif
Peut correspondre ou pas à un fichier existant.
Peut représenter un fichier ou un répertoire.

Un path est une séquence de noms de répertoires, éventuellement suivie par un nom de fichier.
Si le premier élément est un composant racine, comme / ou C:\ (dépend de l'OS), alors représente un chemin absolu.

L'interface java.nio.file.Path représente un path.

java.nio.file.Paths

La classe java.nio.file.Paths fournit 2 méthodes statiques pour créer un path.
public static Path get(String first, String... more)
public static Path get(URI uri)
La première méthode (avec String) fonctionne de 2 manières, suivant qu'on utilise un ou plusieurs arguments :
Les 2 instructions suivantes sont équivalentes :
Path p1 = Paths.get("/tmp/foo");
Path p2 = Paths.get("/", "tmp", "foo");
La seconde méthode attend une uri.
Path p3 = Paths.get(URI.create("file:///Users/joe/FileTest.java"));
(code dans CreatePathExamples.java)
import java.nio.file.Path;
import java.nio.file.Paths;

class PathExamples{
    
    public static void main(String[] args){
        
        // sous Windows : Path home = Paths.get("C:\\Users\\Moi");
        Path home = Paths.get("/home/moi");
        System.out.println(home);    // /home/moi
        
        Path p1 = home.resolve("dev/myapp/src");
        // Equivalent à : Path p1 = home.resolve(Paths.get("dev/myapp/src"));
        System.out.println(p1);    // /home/moi/dev/myapp/src
        
        Path p2 = p1.resolveSibling("tmp.txt");
        System.out.println(p2);    // /home/moi/dev/myapp/tmp.txt
        
        Path p3 = p1.relativize(Paths.get("/home/moi/dev/myapp2/src"));
        System.out.println(p3);    // ../../myapp2/src
        
        // part de l'endroit où a été démarrée la JVM
        Path p4 = Paths.get("config").toAbsolutePath();
        System.out.println(p4);    // /home/thierry/dev/jobs/cnam/git-repos/public/exemples/java/nio/config
        
        Path test;
        test = p2.getParent();
        System.out.println(test);       // /home/moi/dev/myapp
        test = p2.getFileName(); // le dernier élément
        System.out.println(test);       // tmp.txt
        test = p2.getRoot(); // "/" ou "C:\" ou null pour un chemin relatif
        System.out.println(test);       // /
        test = p2.getName(0); // le premier element
        System.out.println(test);       // home
        test = p2.subpath(1, p2.getNameCount()); // tout sauf le premier élément
        System.out.println(test);       // moi/dev/myapp/tmp.txt
        
        // Utilise le fait que Path extends Iterable<Path>
        for (Path component : p4) {
            System.out.print(component + " - ");
        }
        System.out.println();
        // home - thierry - dev - jobs - cnam - git-repos - public - exemples - java - nio - config - 
    }
}
(code dans PathExamples.java)
Remarquer qu'aucune IOException n'est lancée.

Files

On peut toujours travailler avec des java.io.File ou directement avec des java.nio.file.Path.
Pour passer de l'un à l'autre, on peut utiliser Path.toFile() ou File.toPath() (since java 1.7).

La classe principale est java.nio.file.Files, qui ne contient que des méthodes statiques.
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.Files;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.FileVisitResult;
import java.nio.file.attribute.BasicFileAttributes;

import java.io.IOException;
import java.io.PrintWriter;

class FilesExamples{
    
    public static void main(String[] args){
        try{
            String input;
            
            // Créer un nouveau fichier
            // touch toto1.txt
            Path p1 = Paths.get("toto1.txt").toAbsolutePath();
            Files.createFile(p1); // que si n'existe pas (sinon FileAlreadyExistsException)
            
            // Ajouter du contenu dans un fichier
            // cat "du contenu" > toto1.txt
            String content = "du contenu";
            PrintWriter out = new PrintWriter(p1.toFile()); // TWR préférable ici (pas besoin de close())
            out.println(content);
            out.close();
            
            // Autre manière :
            Files.writeString(p1, content);
            
            // Lire le contenu d'un fichier
            String myContent = Files.readString(p1);
            
            // Renommer un fichier
            // cp toto1.txt toto2.txt
            Path p2 = p1.resolveSibling("toto2.txt");
            Files.copy(p1, p2);
            
            System.out.print("toto1.txt et toto2.txt ont été créés - appuyez sur 'Enter' pour les effacer");
            input = System.console().readLine();
            
            // Effacer un fichier
            // rm toto1.txt toto2.txt
            Files.delete(p1);
            Files.delete(p2);
            
            // Créer des répertoires
            // mkdir -p test/{dir1,dir2,dir3/dir4}
            Files.createDirectories(p1.resolveSibling("test/dir1"));
            Files.createDirectories(p1.resolveSibling("test/dir2"));
            Files.createDirectories(p1.resolveSibling("test/dir3/dir4"));
            
            Path test = p1.resolveSibling("test");
            
            // Parcourir une hiérarchie de fichiers
            // for i in $(tree -fi test | grep test); do touch $i/test.txt; done;
            Files.walkFileTree(test, new SimpleFileVisitor() {
                 @Override public FileVisitResult postVisitDirectory(Path dir, IOException e) throws IOException {
                     if (e == null) {
                         Files.createFile(dir.resolve("test.txt"));
                         return FileVisitResult.CONTINUE;
                     } else {
                         throw e; // directory iteration failed
                     }
                 }
             });
            
            System.out.print("test/ a été créé - appuyez sur 'Enter' pour l'effacer");
            input = System.console().readLine();
            
            // Supprimer récursivement un répertoire
            // rm -fr test/
            Files.walkFileTree(test, new SimpleFileVisitor() {
                 @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
                     Files.delete(file);
                     return FileVisitResult.CONTINUE;
                 }
                 @Override public FileVisitResult postVisitDirectory(Path dir, IOException e) throws IOException {
                     if (e == null) {
                         Files.delete(dir);
                         return FileVisitResult.CONTINUE;
                     } else {
                         throw e; // directory iteration failed
                     }
                 }
             });
        }
        catch(IOException e){
            e.printStackTrace();
        }
                
    }
}
(code dans dans FilesExamples.java).

Autres méthodes utiles de Files:
createDirectories(path)
createDirectory(path)
createFile(path)

exists(path)

isDirectory(path)
isRegularFile(path)
Certaines fonctions utilisent des enums du package :
AccessMode
FileVisitOption
FileVisitResult
LinkOption
StandardCopyOption
StandardOpenOption