Écrire des fichiers en Java
Écrivez des fichiers texte et binaires en Java avec FileWriter, BufferedWriter, PrintWriter et Files.writeString.
Écrire un fichier en Java consiste à convertir des données en mémoire — une String, une List de lignes ou un byte[] — en octets sur le disque. Ce chapitre couvre les cinq writers que vous utiliserez réellement, dans quelles situations chacun est adapté, les options StandardOpenOption qui déterminent le comportement d'écrasement ou d'ajout, et le bug d'écriture le plus courant : des données qui "n'ont pas été sauvegardées" parce que le writer n'a jamais été fermé.
L'écriture suit la même structure que la lecture dans le chapitre précédent — des méthodes one-liner modernes par-dessus Files, des décorateurs classiques par-dessus FileWriter, et un petit ensemble d'options qui déterminent ce qui se passe lorsque le fichier cible existe ou non.
Files.writeString(path, text) — fichier entier, un seul appel
Le pendant de Files.readString. Ajouté en Java 11.
Files.writeString(Path.of("notes.txt"), "hello world\n", StandardCharsets.UTF_8);Les options d'ouverture par défaut sont CREATE, WRITE, TRUNCATE_EXISTING — c'est-à-dire "créer si absent, écraser si présent". Ce comportement par défaut surprend ceux qui s'attendent à un mode ajout ; vous devez l'activer explicitement :
Files.writeString(path, "another line\n", StandardCharsets.UTF_8,
StandardOpenOption.CREATE, StandardOpenOption.APPEND);Retourne le Path que vous lui avez fourni (pratique pour le chaînage). À utiliser quand : vous avez une petite quantité de texte et souhaitez un seul appel. Même mise en garde mémoire que readString — ne construisez pas une string de 4 Go en mémoire juste pour l'écrire.
Files.write(path, lines) et Files.write(path, bytes)
Deux surcharges du même Files.write :
Files.write(Path.of("hosts.txt"), List.of("alpha", "beta", "gamma"), StandardCharsets.UTF_8);
Files.write(Path.of("photo.png"), pngBytes);La surcharge Iterable<? extends CharSequence> écrit chaque élément sur sa propre ligne avec des séparateurs \n. La surcharge byte[] écrit des octets bruts — c'est l'option idéale pour les données binaires lorsque les octets sont déjà en mémoire.
Files.newBufferedWriter(path) — la fabrique de writers moderne
Le pendant basé sur les handles et en streaming de Files.newBufferedReader.
try (BufferedWriter w = Files.newBufferedWriter(
Path.of("out.txt"), StandardCharsets.UTF_8, StandardOpenOption.CREATE)) {
w.write("first line");
w.newLine();
w.write("second line");
w.newLine();
}À utiliser quand : vous écrivez de nombreux petits fragments (une boucle sur des enregistrements, une transformation en streaming, un writer de log) et ne souhaitez pas matérialiser tout le contenu en une string d'abord. Le buffer regroupe les écritures de sorte que l'OS voit quelques grands appels système au lieu de nombreux petits.
FileWriter et BufferedWriter — la pile classique
La version legacy "construite à la main" :
try (BufferedWriter w = new BufferedWriter(new FileWriter("out.txt", StandardCharsets.UTF_8))) {
for (String line : lines) {
w.write(line);
w.newLine();
}
}Trois couches, de bas en haut : FileWriter écrit des caractères bruts en utilisant le charset que vous fournissez (ou le défaut de la plateforme — ne faites jamais cela) ; BufferedWriter l'enveloppe avec un buffer en mémoire et une méthode newLine() portable. Même structure, plus de code que la forme Files.newBufferedWriter. Le nouveau code préfère la fabrique moderne ; vous verrez cette pile dans du code plus ancien.
Le deuxième argument du constructeur de FileWriter est append :
new FileWriter("out.txt", true); // append mode (boolean)
new FileWriter("out.txt", StandardCharsets.UTF_8); // overwrite, UTF-8
new FileWriter("out.txt", StandardCharsets.UTF_8, true); // append, UTF-8Le constructeur (String, boolean) est antérieur aux constructeurs avec gestion du charset. Mélanger les deux dans la même base de code est l'un de ces risques de maintenance liés à l'héritage — même classe, deux ordres d'arguments concurrents.
PrintWriter — sortie formatée
PrintWriter ajoute print, println et printf par-dessus n'importe quel Writer. C'est la même API que vous utilisez sur System.out (qui est lui-même un PrintStream, le pendant orienté octets).
try (PrintWriter w = new PrintWriter(Files.newBufferedWriter(Path.of("report.txt")))) {
w.println("Report generated");
w.printf("user = %-10s total = %d%n", "alice", 42);
w.printf("user = %-10s total = %d%n", "bob", 17);
}Deux points à retenir :
printfutilise%npour le séparateur de ligne de la plateforme.\nest un LF codé en dur, ce qui est généralement ce que vous souhaitez pour les fichiers log et les données lues par des machines.PrintWriteravale lesIOException.print,printlnetprintfne lancent pas d'exception — ils définissent un indicateur d'erreur interne que vous vérifiez aveccheckError(). C'est un choix délibéré pourSystem.out(les écritures console ne doivent pas faire planter un outil CLI), mais c'est une source de bugs pour les writers de fichiers. Si la gestion fiable des erreurs est importante, passezfalseau constructeur approprié et utilisezBufferedWriterpour l'écriture,PrintWriteruniquement pour les helpers de formatage — ou interrogezcheckError()après les écritures.
Les options StandardOpenOption
Chaque writer moderne accepte des varargs OpenOption... qui modifient la sémantique d'ouverture :
| Option | Signification |
|---|---|
CREATE | Créer le fichier s'il n'existe pas ; sinon ouvrir l'existant. |
CREATE_NEW | Créer ; lever FileAlreadyExistsException si le fichier existe. Atomique. |
TRUNCATE_EXISTING | Si le fichier existait, le vider à l'ouverture. |
APPEND | Écrire à la fin du fichier sans le tronquer. Atomique sur la plupart des OS. |
WRITE | Ouvrir en écriture. Toujours impliqué pour les writers. |
SYNC / DSYNC | Bloquer chaque écriture jusqu'à ce que l'OS confirme qu'elle est sur le disque. Lent ; durabilité pour la sécurité en cas de crash. |
DELETE_ON_CLOSE | Supprimer le fichier à la fermeture du flux. |
Les combinaisons qui comptent :
- Écraser (par défaut) :
CREATE, TRUNCATE_EXISTING. Ce queFiles.writeStringetFiles.newBufferedWriterutilisent par défaut. - Ajouter :
CREATE, APPEND. Le pattern pour les fichiers log. - Créer ou échouer :
CREATE_NEW. Le pattern pour les fichiers verrou ou "ne pas écraser".
APPEND est atomique au niveau de l'OS sur Unix : deux processus qui ajoutent au même fichier obtiennent des blocs entrelacés mais pas d'écriture partielle à l'intérieur d'un seul fragment bufférisé. C'est le contrat qui en fait le pattern standard pour la journalisation.
Le piège du "writer n'a rien écrit"
C'est le bug que rencontre chaque base de code Java une fois :
// WRONG — the writer is never closed
BufferedWriter w = Files.newBufferedWriter(path);
w.write("important data");
return; // tail buffer is still in memory; nothing reached the diskBufferedWriter (et PrintWriter) regroupe les écritures dans un fragment en mémoire. Les octets n'atteignent pas le disque tant que le buffer n'est pas plein ou que close() n'est pas exécuté. Sans try-with-resources vous sautez la fermeture, et vos données "sauvegardées" s'évaporent.
// CORRECT
try (BufferedWriter w = Files.newBufferedWriter(path)) {
w.write("important data");
} // close() runs here; tail buffer is flushedSi vous avez besoin des données sur le disque avant la fermeture — par exemple, un observateur de queue doit voir chaque ligne de log — appelez flush() explicitement. Files.newBufferedWriter ne vide pas automatiquement après chaque écriture ; c'est le prix du buffer.
Quel writer utiliser
| Scénario | Choisir |
|---|---|
| Petite string, en une fois | Files.writeString |
| Liste de lignes ou tableau d'octets | Files.write |
| Streaming de nombreuses lignes | Files.newBufferedWriter |
Besoin du formatage printf | PrintWriter enveloppant un writer bufférisé |
| Code legacy uniquement | BufferedWriter(new FileWriter(...)) |
Par défaut, utilisez Files.writeString pour "j'ai déjà le texte" et Files.newBufferedWriter pour "je vais le construire ligne par ligne". Utilisez PrintWriter uniquement quand vous avez besoin de printf.
Un exemple concret : tous les writers côte à côte
Le programme ci-dessous écrit le même contenu de trois manières différentes — one-shot moderne, streaming ligne par ligne via BufferedWriter, et formatage printf via PrintWriter — puis démontre APPEND par rapport au défaut TRUNCATE_EXISTING, et enfin le mode de défaillance "oubli de fermeture". Toutes les écritures ciblent un fichier temporaire pour que l'exemple s'exécute n'importe où.
Ce qu'il faut retenir de l'exécution :
Files.writeStringetFiles.write(List)sont les bons appels quand vous avez déjà tout votre contenu. Les deux ont écrasé le fichier à chaque fois car leurs options par défaut incluentTRUNCATE_EXISTING.BufferedWriteretPrintWriteront été exécutés dans des blocstry-with-resources. C'est la seule chose qui garantit que le buffer de queue atteint le disque — sautez-le et vous introduisez un bug "writer n'a rien écrit".- La séquence APPEND/TRUNCATE a écrit
base, ajoutéappended, puis tronqué et écrittruncated. Le fichier final ne contenait quetruncated\n, ce qui est le piège — le mode par défaut de chaque writer moderne est d'écraser, pas d'ajouter. Vous devez l'activer explicitement. CREATE_NEWsur un chemin existant a lancéFileAlreadyExistsException. C'est la sémantique "ne pas écraser" — utile pour les fichiers verrou et les marqueurs atomiques "ai-je déjà exécuté ?".- Le writer qui fuit avait une taille de fichier de 0 avant l'exécution de
flush(). Les octets étaient en mémoire, pas sur le disque ; sans leflush()manuel (ou unclose()approprié), ils auraient été perdus.
Étape suivante
Le prochain chapitre, Supprimer des fichiers en Java, conclut les chapitres sur les "opérations de fichiers de haut niveau" avec les trois méthodes de suppression : File.delete(), Files.delete() et Files.deleteIfExists() — et comment supprimer une arborescence de répertoires sans écrire la récursion à la main.