Classe Java NIO Files
Opérations sur le système de fichiers Java avec java.nio.file.Files — lire, écrire, copier, déplacer, parcourir.
Path (le chapitre précédent) était le nom. Files est le verbe — une classe utilitaire statique dont chaque méthode prend un Path et fait quelque chose au fichier à ce chemin. C'est la maison des one-liners qui ont discrètement raccourci le reste de cette partie : Files.readString, Files.newBufferedReader, Files.createTempFile, Files.size. Ce chapitre parcourt le catalogue complet.
Files est grande — environ 80 méthodes — et regroupée par objectif : lire, écrire, créer, inspecter, modifier, parcourir. Vous n'avez pas besoin de la mémoriser ; il faut savoir que c'est le premier endroit à consulter quand vous voulez faire quoi que ce soit avec un fichier.
Lire
Les lecteurs de fichiers entiers tiennent en une ligne chacun :
String text = Files.readString(path); // UTF-8 by default (Java 11+)
String utf16 = Files.readString(path, StandardCharsets.UTF_16);
byte[] bytes = Files.readAllBytes(path);
List<String> lines = Files.readAllLines(path, StandardCharsets.UTF_8);Pour les fichiers suffisamment petits pour tenir en mémoire, readString et readAllBytes sont les bons outils. Ils ouvrent le fichier, lisent tout, ferment, et vous remettent le contenu. Pas de flux, pas de tampons, pas de logique de fermeture.
Pour les fichiers trop volumineux pour être chargés en entier, utilisez les formes en flux :
try (BufferedReader r = Files.newBufferedReader(path, StandardCharsets.UTF_8)) {
String line;
while ((line = r.readLine()) != null) process(line);
}
try (Stream<String> lines = Files.lines(path, StandardCharsets.UTF_8)) {
lines.filter(...).forEach(...); // closes the file when the stream closes
}
try (InputStream in = Files.newInputStream(path)) {
// raw bytes for binary formats
}Files.lines est BufferedReader.lines avec la plomberie d'ouverture-fermeture encapsulée. Le try-with-resources autour du Stream effectue la fermeture — sans lui, le descripteur de fichier fuit.
Écrire
Même structure du côté écriture :
Files.writeString(path, "hello\n", StandardCharsets.UTF_8);
Files.write(path, bytes); // byte[]
Files.write(path, lines, StandardCharsets.UTF_8); // Iterable<? extends CharSequence>Les trois sont des atomiques à un seul appel : ouvrir, écrire, fermer. Par défaut, ils créent ou tronquent — si le fichier existait, son contenu précédent est supprimé. Pour ajouter :
Files.writeString(path, "more\n", StandardCharsets.UTF_8, StandardOpenOption.APPEND);Pour la forme en flux (écriture incrémentielle) :
try (BufferedWriter w = Files.newBufferedWriter(path, StandardCharsets.UTF_8)) {
for (String line : lines) w.write(line);
}Options d'ouverture
Chaque méthode de lecture/écriture qui ouvre un fichier accepte un varargs optionnel de StandardOpenOption :
| Option | Signification |
|---|---|
READ | Ouvrir en lecture |
WRITE | Ouvrir en écriture |
CREATE | Créer si absent ; ne rien faire si présent |
CREATE_NEW | Créer si absent ; échouer si présent |
APPEND | Les écritures vont à la fin du fichier |
TRUNCATE_EXISTING | Effacer le contenu à l'ouverture |
DELETE_ON_CLOSE | Supprimer quand le canal se ferme (fichiers temporaires) |
SYNC / DSYNC | Bloquer les écritures jusqu'à ce que l'OS confirme que les données sont sur le disque |
Le mode d'ouverture par défaut pour newBufferedWriter et writeString est CREATE, TRUNCATE_EXISTING, WRITE. Le défaut pour newBufferedReader et readString est READ. Les options explicites remplacent les valeurs par défaut — passer n'importe quelle option désactive l'ensemble implicite, donc vous devez généralement répéter les options implicites lorsque vous personnalisez :
Files.newBufferedWriter(path, StandardCharsets.UTF_8,
StandardOpenOption.CREATE,
StandardOpenOption.APPEND); // appends, creates if absentCréer
Files.createFile(path); // empty file; fails if it exists
Files.createDirectory(path); // single dir; fails if parent absent
Files.createDirectories(path); // recursive: like `mkdir -p`
Files.createSymbolicLink(link, target);
Files.createLink(link, target); // hard link
Path tmpFile = Files.createTempFile("prefix-", ".txt"); // in the default temp dir
Path tmpDir = Files.createTempDirectory("prefix-");createDirectories est le bon outil pour « je veux que ce répertoire existe ». Il est idempotent : si le répertoire est déjà là, il retourne sans erreur ; si un ancêtre est absent, il crée toute la chaîne. createDirectory (sans -ies) ne fait qu'un seul niveau et échoue si le parent n'existe pas — presque toujours incorrect sauf si vous avez spécifiquement besoin de cette vérification.
Pour les fichiers temporaires, les surcharges createTempFile et createTempDirectory choisissent automatiquement le répertoire temporaire système et retournent le Path créé. Associez-les à .toFile().deleteOnExit() pour le nettoyage, ou faites un Files.delete explicite dans un finally.
Inspecter
Les prédicats et les accesseurs :
boolean ok = Files.exists(path);
boolean nope = Files.notExists(path); // NOT the negation of exists
boolean file = Files.isRegularFile(path);
boolean dir = Files.isDirectory(path);
boolean link = Files.isSymbolicLink(path);
boolean read = Files.isReadable(path);
boolean write = Files.isWritable(path);
boolean exec = Files.isExecutable(path);
long size = Files.size(path); // throws IOException
FileTime mtime = Files.getLastModifiedTime(path);
String mimeType = Files.probeContentType(path); // best-effort, can return null
UserPrincipal owner = Files.getOwner(path);exists et notExists ne sont pas des négations : les deux peuvent retourner false quand l'accès au fichier ne peut pas être déterminé (permission refusée, lien symbolique cassé). Utilisez le bon pour ce que vous voulez — !exists(p) et notExists(p) diffèrent dans les cas limites.
Copier, déplacer, supprimer
Files.copy(source, target); // fails if target exists
Files.copy(source, target, StandardCopyOption.REPLACE_EXISTING);
Files.copy(source, target,
StandardCopyOption.REPLACE_EXISTING,
StandardCopyOption.COPY_ATTRIBUTES); // copy mtime/owner too
Files.move(source, target, StandardCopyOption.REPLACE_EXISTING);
Files.move(source, target, StandardCopyOption.ATOMIC_MOVE); // rename within a filesystem; rename-or-fail
Files.delete(path); // throws if absent
boolean deleted = Files.deleteIfExists(path); // idempotentFiles.move avec ATOMIC_MOVE est le bon outil pour « écrire dans un fichier temporaire, puis remplacer atomiquement le fichier actif ». Sur le même système de fichiers, cela correspond à rename(2) ; le fichier actif bascule de l'ancien vers le nouveau en un instant, sans état intermédiaire. C'est ainsi que vous construisez des écritures sûres en cas de crash :
Path tmp = path.resolveSibling(path.getFileName() + ".tmp");
Files.writeString(tmp, content, StandardCharsets.UTF_8);
Files.move(tmp, path, StandardCopyOption.ATOMIC_MOVE,
StandardCopyOption.REPLACE_EXISTING);Si la JVM meurt après writeString mais avant move, le fichier actif est intact.
Lister et parcourir
try (Stream<Path> entries = Files.list(directory)) {
entries.forEach(System.out::println); // direct children only
}
try (Stream<Path> tree = Files.walk(directory)) {
tree.filter(Files::isRegularFile).forEach(...); // recursive
}
try (Stream<Path> tree = Files.walk(directory, 2)) { // depth-limited
...
}
try (Stream<Path> found = Files.find(directory, Integer.MAX_VALUE,
(p, attrs) -> attrs.isRegularFile() && p.toString().endsWith(".log"))) {
...
}Utilisez toujours try-with-resources autour de ceux-ci — le DirectoryStream sous-jacent est ouvert jusqu'à ce que le Stream se ferme. Oubliez la fermeture et la JVM maintient un descripteur de répertoire jusqu'à ce que le ramasse-miettes l'remarque, ce qui sur un processus longue durée signifie « jamais ». Le chapitre suivant, Java Walk File Tree, approfondit le parcoureur.
Pourquoi ce chapitre est court
Files n'a pas besoin de beaucoup de narration. Chaque méthode fait une chose, les noms sont descriptifs, les paramètres sont Path, Charset et Option. La charge cognitive est dans le catalogue — savoir ce qui est disponible — pas dans le comportement d'une méthode individuelle. Parcourez la Javadoc de java.nio.file.Files une fois ; revenez-y quand vous avez besoin d'un verbe que vous ne vous rappelez plus.
Un exemple concret : le cycle de vie complet
Le programme ci-dessous crée un répertoire temporaire, écrit un petit fichier texte avec writeString, le relit avec readString, ajoute du contenu avec la bonne option d'ouverture, copie le fichier, le déplace atomiquement, liste le répertoire à chaque étape, et nettoie finalement avec deleteIfExists. C'est le cycle de vie quotidien des fichiers Java compressé en une seule méthode main.
Ce qu'il faut retenir de l'exécution :
Files.writeString(...)a ouvert le fichier, écrit le contenu, et l'a fermé — un seul appel là oùjava.ioaurait vouluFileOutputStream+OutputStreamWriter(UTF-8)+BufferedWriter+try-with-resources. Le comportement par défaut de troncature à l'ouverture est exactement ce que « sauvegarder ce contenu » attend. Quand vous avez besoin de conserver le contenu existant, leStandardOpenOption.APPENDexplicite (passé avecWRITE) est la surcharge.Files.lines(log).filter(...)a effectué le même travail de lecture en flux queBufferedReader.lines()avec la plomberie d'ouverture-fermeture encapsulée. Letry-with-resources autour duStreamest le mécanisme de fermeture — oubliez-le et le descripteur de fichier fuit. Chaque méthode deFilesqui retourne unStreamest fermable ; traitez-la ainsi.- L'étape de copie a utilisé à la fois
REPLACE_EXISTING(autoriser l'écrasement) etCOPY_ATTRIBUTES(transporter le mtime/owner). SansCOPY_ATTRIBUTES, la sauvegarde aurait un mtime récent, ce qui importe pour les vérifications « cette sauvegarde est-elle encore à jour ? ».Files.copyadopte le comportement conservateur par défaut ; vous optez pour autre chose explicitement. - Le bloc de déplacement atomique est le patron d'écriture sûre : écrire le contenu dans
target.tmp, puisATOMIC_MOVEsur le nom actif. Si la JVM plante pendant l'écriture, le fichier actif reste inchangé ; si le renommage réussit, le fichier actif bascule en un instant. Sur le même système de fichiers, cela correspond àrename(2)— il n'y a pas d'étape de copie. Utilisez ceci pour tout fichier où les lecteurs ne doivent jamais voir un état à moitié écrit (configuration, fichiers de sauvegarde, assets générés). Files.walk(dir)a produit unStream<Path>de chaque entrée sous le répertoire en ordre de profondeur d'abord. Le nettoyage à l'étape 10 a trié en sens inverse pour que les enfants soient supprimés avant les parents — la même astuce que vous utiliseriez avec une suppression récursive réelle. (L'assistant de suppression complète d'arborescence vit dans le chapitre suivant souswalkFileTree; la forme en flux ici est la version plus courte pour les petites arborescences.)
Et ensuite
Files couvre les opérations qui agissent sur un seul fichier ou un seul niveau de répertoire. Le chapitre suivant, Java Walk File Tree, approfondit la traversée d'une arborescence complète — Files.walkFileTree, FileVisitor, ignorer des sous-arborescences, l'API de patron visiteur qui gère les cas que la forme Stream ne peut pas traiter.