W3docs

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 :

OptionSignification
READOuvrir en lecture
WRITEOuvrir en écriture
CREATECréer si absent ; ne rien faire si présent
CREATE_NEWCréer si absent ; échouer si présent
APPENDLes écritures vont à la fin du fichier
TRUNCATE_EXISTINGEffacer le contenu à l'ouverture
DELETE_ON_CLOSESupprimer quand le canal se ferme (fichiers temporaires)
SYNC / DSYNCBloquer 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 absent

Cré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);                       // idempotent

Files.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.

java— editable, runs on the server

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.io aurait voulu FileOutputStream + 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, le StandardOpenOption.APPEND explicite (passé avec WRITE) est la surcharge.
  • Files.lines(log).filter(...) a effectué le même travail de lecture en flux que BufferedReader.lines() avec la plomberie d'ouverture-fermeture encapsulée. Le try-with-resources autour du Stream est le mécanisme de fermeture — oubliez-le et le descripteur de fichier fuit. Chaque méthode de Files qui retourne un Stream est fermable ; traitez-la ainsi.
  • L'étape de copie a utilisé à la fois REPLACE_EXISTING (autoriser l'écrasement) et COPY_ATTRIBUTES (transporter le mtime/owner). Sans COPY_ATTRIBUTES, la sauvegarde aurait un mtime récent, ce qui importe pour les vérifications « cette sauvegarde est-elle encore à jour ? ». Files.copy adopte 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, puis ATOMIC_MOVE sur 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 un Stream<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 sous walkFileTree ; 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.

Pratique

Pratique
Vous voulez écraser le fichier `/var/data/config.json` avec un nouveau contenu, mais les lecteurs ne doivent jamais voir un état à moitié écrit si la JVM plante pendant l'écriture. Quelle séquence d'appels `Files` implémente le patron d'écriture sûre ?
Vous voulez écraser le fichier `/var/data/config.json` avec un nouveau contenu, mais les lecteurs ne doivent jamais voir un état à moitié écrit si la JVM plante pendant l'écriture. Quelle séquence d'appels `Files` implémente le patron d'écriture sûre ?
Was this page helpful?