W3docs

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-8

Il 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:

  • printf usa %n per 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.
  • PrintWriter inghiotte le IOException. print, println e printf non lanciano eccezioni — impostano un flag di errore interno che puoi verificare con checkError(). È una scelta deliberata per System.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, passa false al costruttore appropriato e usa BufferedWriter per la scrittura e PrintWriter solo come helper per la formattazione — oppure interroga checkError() dopo le scritture.

Flag StandardOpenOption

Ogni writer moderno accetta vararg OpenOption... che modificano la semantica di apertura:

OpzioneSignificato
CREATECrea il file se non esiste; altrimenti apre quello esistente.
CREATE_NEWCrea; lancia FileAlreadyExistsException se il file esiste. Atomico.
TRUNCATE_EXISTINGSe il file esiste, lo svuota all'apertura.
APPENDScrive alla fine del file senza troncare. Atomico sulla maggior parte dei sistemi operativi.
WRITEApre in scrittura. Sempre implicito per i writer.
SYNC / DSYNCBlocca ogni scrittura finché il sistema operativo non conferma che è su disco. Lento; durabilità per la sicurezza in caso di crash.
DELETE_ON_CLOSEElimina il file alla chiusura dello stream.

Le combinazioni più importanti:

  • Sovrascrittura (predefinita): CREATE, TRUNCATE_EXISTING. Ciò che usano per default Files.writeString e Files.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 disk

BufferedWriter (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 flushed

Se 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

ScenarioScelta
Stringa piccola, una sola operazioneFiles.writeString
Lista di righe o array di byteFiles.write
Streaming di molte righeFiles.newBufferedWriter
Formattazione con printfPrintWriter che avvolge un writer bufferizzato
Solo per codice legacyBufferedWriter(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.

java— editable, runs on the server

Cosa trarre dall'esecuzione:

  • Files.writeString e Files.write(List) sono le chiamate giuste quando hai già tutto il contenuto. Entrambi hanno sovrascritto il file ogni volta perché le loro opzioni predefinite includono TRUNCATE_EXISTING.
  • BufferedWriter e PrintWriter sono stati eseguiti all'interno di try-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, aggiunto appended, poi troncato e scritto truncated. Il file finale conteneva solo truncated\n, che è la trappola — la modalità predefinita di ogni writer moderno è sovrascrivere, non aggiungere. Devi optare esplicitamente per l'aggiunta.
  • CREATE_NEW su un path esistente ha lanciato FileAlreadyExistsException. 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 il flush() manuale (o un close() 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.

Pratica

Pratica
`Files.writeString(path, text)` senza argomenti `OpenOption`. Cosa fa se il file esiste già?
`Files.writeString(path, text)` senza argomenti `OpenOption`. Cosa fa se il file esiste già?
Was this page helpful?