Classe Java NIO Files
Operazioni sul file system in Java con java.nio.file.Files — lettura, scrittura, copia, spostamento e navigazione delle directory.
Path (il capitolo precedente) era il sostantivo. Files è il verbo — una classe di utilità statica i cui metodi prendono ciascuno un Path ed eseguono qualcosa sul file corrispondente. È la casa delle istruzioni a riga singola che hanno silenziosamente accorciato il resto di questa sezione: Files.readString, Files.newBufferedReader, Files.createTempFile, Files.size. Questo capitolo percorre l'intero catalogo.
Files è estesa — circa 80 metodi — e raggruppata per scopo: lettura, scrittura, creazione, ispezione, modifica, navigazione. Non è necessario memorizzarla; bisogna sapere che è il primo posto dove cercare quando si vuole fare qualsiasi cosa con un file.
Lettura
I lettori di file interi sono di una riga ciascuno:
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);Per file abbastanza piccoli da entrare in memoria, readString e readAllBytes sono gli strumenti giusti. Aprono il file, lo leggono interamente, lo chiudono e restituiscono il contenuto. Niente stream, niente buffer, niente logica di chiusura.
Per file troppo grandi da caricare interamente, si usano le forme in streaming:
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 è BufferedReader.lines con la gestione di apertura e chiusura inclusa. Il blocco try-with-resources attorno allo Stream si occupa della chiusura — senza di esso, l'handle del file non viene rilasciato.
Scrittura
La stessa struttura sul lato della scrittura:
Files.writeString(path, "hello\n", StandardCharsets.UTF_8);
Files.write(path, bytes); // byte[]
Files.write(path, lines, StandardCharsets.UTF_8); // Iterable<? extends CharSequence>Tutti e tre sono operazioni atomiche in una chiamata: apertura, scrittura, chiusura. Per impostazione predefinita creano o troncano — se il file esisteva, il suo contenuto precedente viene eliminato. Per accodare:
Files.writeString(path, "more\n", StandardCharsets.UTF_8, StandardOpenOption.APPEND);Per la forma in streaming (scrittura incrementale):
try (BufferedWriter w = Files.newBufferedWriter(path, StandardCharsets.UTF_8)) {
for (String line : lines) w.write(line);
}Opzioni di apertura
Ogni metodo di lettura/scrittura che apre un file accetta un varargs opzionale di StandardOpenOption:
| Opzione | Significato |
|---|---|
READ | Apri in lettura |
WRITE | Apri in scrittura |
CREATE | Crea se assente; non fare nulla se presente |
CREATE_NEW | Crea se assente; fallisce se presente |
APPEND | Le scritture vanno alla fine del file |
TRUNCATE_EXISTING | Svuota il contenuto all'apertura |
DELETE_ON_CLOSE | Elimina quando il canale viene chiuso (file temporanei) |
SYNC / DSYNC | Blocca le scritture finché l'OS conferma che i dati sono su disco |
La modalità di apertura predefinita per newBufferedWriter e writeString è CREATE, TRUNCATE_EXISTING, WRITE. Il default per newBufferedReader e readString è READ. Le opzioni esplicite sovrascrivono i default — passare qualsiasi opzione disabilita l'insieme implicito, quindi in genere è necessario ripetere quelle implicite quando si personalizza:
Files.newBufferedWriter(path, StandardCharsets.UTF_8,
StandardOpenOption.CREATE,
StandardOpenOption.APPEND); // appends, creates if absentCreazione
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 è lo strumento giusto per "voglio che questa directory esista." È idempotente: se la directory è già presente, restituisce senza errori; se manca qualche antenato, crea l'intera catena. createDirectory (senza -ies) crea solo un livello e fallisce se il padre non esiste — quasi sempre sbagliato a meno che non si abbia specificamente bisogno di quel controllo.
Per i file temporanei, gli overload createTempFile e createTempDirectory scelgono automaticamente la directory temporanea di sistema e restituiscono il Path creato. Abbinarli con .toFile().deleteOnExit() per la pulizia, oppure eseguire Files.delete esplicitamente in un blocco finally.
Ispezione
I predicati e gli accessori:
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 e notExists non sono negazioni l'uno dell'altro: entrambi possono restituire false quando non è possibile determinare l'accesso al file (permesso negato, symlink non valido). Usare quello giusto per ciò che si vuole — !exists(p) e notExists(p) differiscono nei casi limite.
Copia, spostamento, eliminazione
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 con ATOMIC_MOVE è lo strumento giusto per "scrivere su un file temporaneo, poi sostituire atomicamente il file attivo." Sullo stesso filesystem mappa su rename(2); il file attivo passa dal vecchio al nuovo in un istante, senza stati intermedi. Questo è il modo per costruire scritture sicure in caso di 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);Se la JVM si arresta dopo writeString ma prima di move, il file attivo rimane intatto.
Elenco e navigazione
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"))) {
...
}Usare sempre try-with-resources attorno a questi metodi — il DirectoryStream sottostante rimane aperto finché lo Stream non viene chiuso. Omettendo la chiusura, la JVM mantiene un handle alla directory fino a quando il garbage collector non lo nota, il che in un processo a lunga esecuzione equivale a "mai." Il prossimo capitolo, Java Walk File Tree, approfondisce il walker.
Perché questo capitolo è breve
Files non richiede molta narrativa. Ogni metodo fa una cosa sola, i nomi sono descrittivi, i parametri sono Path, Charset e Option. Il carico cognitivo sta nel catalogo — sapere cosa è disponibile — non nel comportamento di un singolo metodo. Scorrere la Javadoc di java.nio.file.Files una volta; tornare quando serve un verbo che non si ricorda.
Un esempio concreto: il ciclo di vita completo
Il programma seguente crea una directory temporanea, scrive un piccolo file di testo con writeString, lo rilegge con readString, accoda con la giusta opzione di apertura, copia il file, lo sposta atomicamente, elenca la directory a ogni passaggio e infine fa pulizia con deleteIfExists. È il ciclo di vita quotidiano di un file Java compresso in un unico metodo main.
Cosa osservare dall'esecuzione:
Files.writeString(...)ha aperto il file, scritto il contenuto e chiuso il tutto — una chiamata dovejava.ioavrebbe richiestoFileOutputStream+OutputStreamWriter(UTF-8)+BufferedWriter+try-with-resources. Il comportamento predefinito di troncamento all'apertura è esattamente quello che vuol dire "salva questo contenuto." Quando si vuole mantenere il contenuto esistente, l'opzione esplicitaStandardOpenOption.APPEND(passata insieme aWRITE) è l'override da usare.Files.lines(log).filter(...)ha svolto lo stesso lavoro di lettura in streaming diBufferedReader.lines(), con la gestione di apertura e chiusura inclusa. Il bloccotry-with-resources attorno alloStreamè il meccanismo di chiusura — ometterlo causa un leak sull'handle del file. Ogni metodo diFilesche restituisce unoStreamè chiudibile; va trattato come tale.- Il passo di copia ha usato sia
REPLACE_EXISTING(permette la sovrascrittura) cheCOPY_ATTRIBUTES(mantiene mtime/owner). SenzaCOPY_ATTRIBUTES, il backup avrebbe un mtime fresco, il che conta per i controlli "questo backup è ancora aggiornato?".Files.copyadotta il comportamento conservativo per default; tutto il resto è opt-in. - Il blocco atomic-move è il pattern di scrittura sicura: scrivi il contenuto su
target.tmp, poi spostalo conATOMIC_MOVEsul nome attivo. Se la JVM si arresta a metà scrittura, il file attivo rimane intatto; se il rename riesce, il file attivo cambia in un istante. Sullo stesso filesystem questo mappa surename(2)— non c'è un passaggio di copia. Usare questo pattern per qualsiasi file i cui lettori non debbano mai vedere uno stato parzialmente scritto (configurazioni, file di salvataggio, asset generati). Files.walk(dir)ha prodotto unStream<Path>di ogni voce sotto la directory in ordine depth-first. La pulizia al passo 10 ha ordinato al contrario così i figli vengono eliminati prima dei genitori — lo stesso trucco che si userebbe con una delete ricorsiva reale. (Il metodo helper per eliminare un albero completo si trova nel prossimo capitolo sottowalkFileTree; la forma in streaming qui è la versione più breve per alberi piccoli.)
Cosa c'è dopo
Files ha coperto le operazioni che agiscono su un singolo file o un singolo livello di directory. Il prossimo capitolo, Java Walk File Tree, approfondisce la navigazione di un intero albero di directory — Files.walkFileTree, FileVisitor, salto dei sottoalberi, l'API con il pattern visitor che gestisce i casi che la forma Stream non può.