Scrivere file in Java
Scrivi file di testo e binari in Java con FileWriter, BufferedWriter, PrintWriter e Files.writeString.
Scrivere un file in Java significa trasformare i dati in memoria — una String, una List di righe o un byte[] — in byte su disco. Questo capitolo tratta i cinque writer che userai effettivamente, quando ciascuno è adatto, i flag StandardOpenOption che determinano il comportamento di sovrascrittura o aggiunta, e il bug di scrittura più comune: dati che "non si sono salvati" perché il writer non è mai stato chiuso.
La scrittura segue la stessa struttura della lettura del capitolo precedente — one-liner moderni basati su Files, decorator classici basati su FileWriter, e un piccolo insieme di opzioni che decidono cosa accade quando il file di destinazione esiste o non esiste.
Files.writeString(path, text) — intero file in una sola chiamata
Il corrispettivo di Files.readString. Aggiunto in Java 11.
Files.writeString(Path.of("notes.txt"), "hello world\n", StandardCharsets.UTF_8);Le opzioni di apertura predefinite sono CREATE, WRITE, TRUNCATE_EXISTING — ovvero "crea se mancante, sovrascrivi se presente." Questa impostazione predefinita sorprende chi si aspetta un comportamento di aggiunta; devi optare esplicitamente per quest'ultima:
Files.writeString(path, "another line\n", StandardCharsets.UTF_8,
StandardOpenOption.CREATE, StandardOpenOption.APPEND);Restituisce il Path fornito (utile per il chaining). Usalo quando: hai una piccola quantità di testo e vuoi una singola chiamata. Stessa avvertenza sulla memoria di readString — non costruire una stringa da 4 GB in memoria solo per scriverla.
Files.write(path, lines) e Files.write(path, bytes)
Due overload dello stesso Files.write:
Files.write(Path.of("hosts.txt"), List.of("alpha", "beta", "gamma"), StandardCharsets.UTF_8);
Files.write(Path.of("photo.png"), pngBytes);L'overload Iterable<? extends CharSequence> scrive ogni elemento su una propria riga con separatori \n. L'overload byte[] scrive byte grezzi — la scelta ideale per dati binari quando i byte sono già in memoria.
Files.newBufferedWriter(path) — la factory moderna per writer
Il corrispettivo basato su handle e streaming di 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();
}Usalo quando: stai scrivendo molti piccoli blocchi (un ciclo su record, una trasformazione in streaming, un writer di log) e non vuoi materializzare l'intero contenuto come stringa prima. Il buffer raggruppa le scritture in modo che il sistema operativo veda un numero limitato di grandi syscall invece di molte piccole.
FileWriter e BufferedWriter — lo stack classico
La versione "costruiscila tu stesso" classica:
try (BufferedWriter w = new BufferedWriter(new FileWriter("out.txt", StandardCharsets.UTF_8))) {
for (String line : lines) {
w.write(line);
w.newLine();
}
}Tre livelli, dal basso verso l'alto: FileWriter scrive caratteri grezzi usando il charset fornito (o quello predefinito della piattaforma — da non fare mai); BufferedWriter lo avvolge con un buffer in memoria e un metodo newLine() portabile. Stessa struttura, più codice rispetto alla forma Files.newBufferedWriter. Il nuovo codice preferisce la factory moderna; troverai questo stack nel codice più vecchio.
Il secondo argomento del costruttore di FileWriter è 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-8Il costruttore (String, boolean) è precedente a quelli con supporto del charset. Mescolare i due nella stessa base di codice è uno di quei rischi di manutenzione legacy — stessa classe, due ordini di argomenti concorrenti.
PrintWriter — output formattato
PrintWriter aggiunge print, println e printf sopra qualsiasi Writer. È la stessa API che hai usato su System.out (che è a sua volta un PrintStream, il sibling orientato ai byte).
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);
}Due cose da sapere:
printfusa%nper il separatore di riga della piattaforma.\nè LF codificato, che è quello che di solito si vuole per file di log e dati leggibili da macchine.PrintWriteringhiotte leIOException.print,printlneprintfnon lanciano eccezioni — impostano un flag di errore interno che puoi verificare concheckError(). È una scelta deliberata perSystem.out(le scritture su console non dovrebbero far crashare uno strumento CLI), ma è un magnete per bug nei writer su file. Se la gestione affidabile degli errori è importante, passafalseal costruttore appropriato e usaBufferedWriterper la scrittura ePrintWritersolo come helper per la formattazione — oppure interrogacheckError()dopo le scritture.
Flag StandardOpenOption
Ogni writer moderno accetta vararg OpenOption... che modificano la semantica di apertura:
| Opzione | Significato |
|---|---|
CREATE | Crea il file se non esiste; altrimenti apre quello esistente. |
CREATE_NEW | Crea; lancia FileAlreadyExistsException se il file esiste. Atomico. |
TRUNCATE_EXISTING | Se il file esiste, lo svuota all'apertura. |
APPEND | Scrive alla fine del file senza troncare. Atomico sulla maggior parte dei sistemi operativi. |
WRITE | Apre in scrittura. Sempre implicito per i writer. |
SYNC / DSYNC | Blocca ogni scrittura finché il sistema operativo non conferma che è su disco. Lento; durabilità per la sicurezza in caso di crash. |
DELETE_ON_CLOSE | Elimina il file alla chiusura dello stream. |
Le combinazioni più importanti:
- Sovrascrittura (predefinita):
CREATE, TRUNCATE_EXISTING. Ciò che usano per defaultFiles.writeStringeFiles.newBufferedWriter. - Aggiunta:
CREATE, APPEND. Il pattern per i file di log. - Crea o fallisci:
CREATE_NEW. Il pattern per lock file o "non sovrascrivere".
APPEND è atomico a livello OS su Unix: due processi che aggiungono allo stesso file ottengono blocchi intercalati ma nessuna scrittura parziale all'interno di un singolo blocco bufferizzato. Questo è il contratto che lo rende il pattern standard di logging.
La trappola del "writer non ha scritto nulla"
Questo è il bug che ogni base di codice Java incontra almeno una volta:
// 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 (e PrintWriter) raggruppa le scritture in un blocco in memoria. I byte non raggiungono il disco finché il buffer non si riempie o non viene eseguito close(). Senza try-with-resources si salta la chiusura, e i dati "salvati" evaporano.
// CORRECT
try (BufferedWriter w = Files.newBufferedWriter(path)) {
w.write("important data");
} // close() runs here; tail buffer is flushedSe hai bisogno dei dati su disco prima della chiusura — ad esempio, un tail-watcher deve vedere ogni riga di log — chiama flush() esplicitamente. Files.newBufferedWriter non esegue il flush automatico dopo ogni scrittura; questo è il prezzo del buffer.
Quale writer scegliere
| Scenario | Scelta |
|---|---|
| Stringa piccola, una sola operazione | Files.writeString |
| Lista di righe o array di byte | Files.write |
| Streaming di molte righe | Files.newBufferedWriter |
Formattazione con printf | PrintWriter che avvolge un writer bufferizzato |
| Solo per codice legacy | BufferedWriter(new FileWriter(...)) |
Usa Files.writeString per "ho già il testo" e Files.newBufferedWriter per "lo costruirò riga per riga." Ricorri a PrintWriter solo quando hai bisogno di printf.
Un esempio pratico: tutti i writer a confronto
Il programma seguente scrive lo stesso contenuto in tre modi diversi — one-shot moderno, streaming riga per riga con BufferedWriter, e formattato con printf tramite PrintWriter — poi dimostra APPEND rispetto al TRUNCATE_EXISTING predefinito, e infine la modalità di fallimento del "dimenticato di chiudere". Tutte le scritture puntano a un file temporaneo in modo che l'esempio possa essere eseguito ovunque.
Cosa trarre dall'esecuzione:
Files.writeStringeFiles.write(List)sono le chiamate giuste quando hai già tutto il contenuto. Entrambi hanno sovrascritto il file ogni volta perché le loro opzioni predefinite includonoTRUNCATE_EXISTING.BufferedWriterePrintWritersono stati eseguiti all'interno ditry-with-resources. È l'unica cosa che garantisce che il buffer finale raggiunga il disco — saltarlo introduce il bug del "writer non ha scritto nulla".- La sequenza APPEND/TRUNCATE ha scritto
base, aggiuntoappended, poi troncato e scrittotruncated. Il file finale conteneva solotruncated\n, che è la trappola — la modalità predefinita di ogni writer moderno è sovrascrivere, non aggiungere. Devi optare esplicitamente per l'aggiunta. CREATE_NEWsu un path esistente ha lanciatoFileAlreadyExistsException. Questa è la semantica "non sovrascrivere" — utile per lock file e marker atomici "ho già eseguito?".- Il writer dimenticato aveva una dimensione di file pari a 0 prima che venisse eseguito
flush(). I byte erano in memoria, non su disco; senza ilflush()manuale (o unclose()corretto), sarebbero andati persi.
Cosa c'è dopo
Il prossimo capitolo, Eliminare file in Java, conclude i capitoli sulle "operazioni di alto livello sui file" con i tre metodi di eliminazione: File.delete(), Files.delete() e Files.deleteIfExists() — e come rimuovere un albero di directory senza scrivere la ricorsione a mano.