W3docs

Introduzione a Java I/O

Panoramica di Java I/O: stream di byte e di caratteri, I/O bufferizzato, java.io vs. java.nio.file.

La Parte 12 si è conclusa con un vocabolario che puoi portare direttamente in questa parte: lambda, Consumer<T> e Supplier<T> come forme di "dammi una riga" e "fai qualcosa con questa riga", try-with-resources per tutto ciò che richiede una chiusura deterministica, e la pipeline Stream per dati orientati alle righe. Le API I/O di Java sono state progettate esattamente attorno a queste forme — molto prima che esistessero le parole "interfaccia funzionale", gli oggetti sottostanti avevano già un solo metodo ciascuno, e la facciata post-Java-8 ha completato il percorso.

Questa parte copre quattro toolkit sovrapposti:

  1. Stream java.io — la API originale di Java 1.0: InputStream/OutputStream per i byte, Reader/Writer per i caratteri, e i decorator Buffered*, Data*, Print* che li avvolgono.
  2. java.io.File — la classe "legacy" che tratta una stringa come un percorso. Ancora presente ovunque nel codice più vecchio; sostituita da java.nio.file.Path per il nuovo codice.
  3. java.nio.file — la API moderna (Java 7+): Path, Files, e gli helper statici (Files.readString, Files.writeString, Files.lines, Files.walk) che riducono la maggior parte delle operazioni sui file a una sola riga.
  4. Serializzazione — trasformare grafi di oggetti in byte e viceversa con ObjectOutputStream / ObjectInputStream.

I primi sei capitoli trattano le operazioni di alto livello sui file (apertura, creazione, lettura, scrittura, eliminazione) usando sia java.io che java.nio.file, così puoi vedere lo stesso compito in due modi. I capitoli centrali approfondiscono le classi stream stesse — byte vs. caratteri, buffering, dati, print. Gli ultimi capitoli coprono la serializzazione e la API path/walk.

La divisione byte/carattere

Ogni API I/O in java.io è una di due forme:

InputStream  /  OutputStream     — byte-oriented   (raw bytes: int read() returns 0..255 or -1)
Reader       /  Writer            — character-oriented (decoded text: int read() returns a char or -1)

La divisione non è cosmética. I byte sono ciò che dischi e socket memorizzano; i caratteri sono ciò che gli esseri umani leggono. Un .png è byte; un .txt è anch'esso byte su disco, ma di solito lo si vuole decodificato in caratteri usando un charset. Mescolare i due senza un charset è la causa più comune di bug con "caratteri strani" nel codice Java legacy.

Le classi bridge — InputStreamReader e OutputStreamWriter — convertono tra i due e accettano un argomento Charset. Usa StandardCharsets.UTF_8 a meno che tu non abbia un motivo documentato per usare altro; le forme senza argomento usano il charset predefinito della piattaforma, che varia tra sistemi operativi ed è la causa classica dei bug "funziona sul mio Mac, rotto sul server Linux".

Il pattern decorator

java.io è costruita sul pattern decorator: un piccolo insieme di stream grezzi (FileInputStream, FileOutputStream, FileReader, FileWriter) avvolti in funzionalità a strati (buffering, testo riga per riga, tipi primitivi, output formattato). Si compone ciò di cui si ha bisogno nel punto di chiamata:

// Read a UTF-8 text file line by line:
try (BufferedReader in = new BufferedReader(
        new InputStreamReader(new FileInputStream("a.txt"), StandardCharsets.UTF_8))) {
  String line;
  while ((line = in.readLine()) != null) {
    System.out.println(line);
  }
}

Tre strati, dal basso: FileInputStream legge i byte grezzi; InputStreamReader li decodifica come caratteri UTF-8; BufferedReader aggiunge un buffer in memoria e il metodo readLine(). Ogni strato è una classe separata con un solo compito. Java 11 ha ridotto questo esatto pattern a una riga — Files.newBufferedReader(path) — ma la decorazione è ancora ciò che avviene internamente.

try-with-resources è la regola

Ogni stream, reader, writer e channel in java.io e java.nio implementa AutoCloseable. Chiuderli è importante: un FileOutputStream non chiuso può perdere il suo buffer finale; un socket non chiuso fa trapelare un file descriptor; un reader non chiuso su Windows mantiene un lock che il sistema operativo non rilascerà. Il costrutto try-with-resources (Java 7+) garantisce che close() venga chiamato su ogni percorso, riuscito o eccezionale:

try (BufferedReader in = Files.newBufferedReader(path)) {
  return in.readLine();
}                                  // close() runs here, even if readLine() throws

Si possono dichiarare più risorse nello stesso try; vengono chiuse in ordine inverso. Il vecchio codice try/finally che chiama close() manualmente è quasi sempre sbagliato — l'eccezione interna ingoia l'eccezione di chiusura, oppure la chiusura stessa viene dimenticata nel percorso di errore. Usa try-with-resources per tutto ciò che apre un handle.

java.io versus java.nio.file

java.io.File (1996) modellava un percorso come una String e offriva un insieme limitato di operazioni (exists, isFile, delete, listFiles). La classe è ancora ovunque nel codice legacy, e molte API restituiscono o accettano ancora File. Ma ha limiti che il JDK non nasconde più:

  • Nessun modo per sapere perché un'operazione ha fallito — file.delete() restituisce false sia per "il file non esiste," sia per "permesso negato," sia per "il file è aperto." Non si può distinguere.
  • Nessun supporto per link simbolici, attributi di file, permessi o operazioni atomiche.
  • Nessun modo per navigare un albero di directory senza scrivere la ricorsione a mano.

java.nio.file (Java 7) lo sostituisce. Path è il nuovo tipo "questo è un percorso", e Files è una classe utility static con circa 80 metodi per tutto ciò che si vuole fare con uno di essi:

Path p = Path.of("data", "users.txt");                       // platform-independent path
String text = Files.readString(p, StandardCharsets.UTF_8);   // whole file, one call
List<String> lines = Files.readAllLines(p, StandardCharsets.UTF_8);
Files.writeString(p, "hello\n", StandardCharsets.UTF_8);
try (Stream<String> s = Files.lines(p, StandardCharsets.UTF_8)) {
  s.filter(l -> !l.isBlank()).forEach(System.out::println);
}

Due cose da notare. Prima, Files.lines(path) restituisce uno Stream<String> — la pipeline stream appresa nella Parte 12 legge i file direttamente. Seconda, lo stream detiene un file handle aperto, quindi il wrapper try-with-resources è obbligatorio — senza di esso, il file rimane aperto fino al prossimo GC.

Nel corso della Parte 13 mostreremo entrambe le API affiancate. Il nuovo codice dovrebbe prima cercare java.nio.file; i capitoli legacy esistono perché incontrerai le forme più vecchie in ogni codebase precedente a Java 11.

Dove sta andando questa parte

  • Il prossimo capitolo, Java File Class, esamina la API java.io.File legacy — i suoi metodi di interrogazione, il listing e i limiti che hanno motivato java.nio.file.
  • I quattro capitoli successivi (Creating Files, Reading Files, Writing Files, Deleting Files) coprono le operazioni di alto livello "fai una cosa con un file" usando entrambe le API.
  • I capitoli su stream di byte, di caratteri e bufferizzati approfondiscono poi lo stack decorator java.io sottostante.
  • La serializzazione, poi Path, Files e la API di navigazione delle directory chiudono la parte.

Un esempio pratico: lo stesso compito in quattro modi

Il programma seguente scrive un breve file di testo in quattro modi — una volta con il moderno Files.writeString, una volta con il classico FileWriter + try-with-resources, una volta decorato con BufferedWriter, e una volta con PrintWriter per l'output formattato. Poi rilegge il file in due modi — una volta con Files.readString (intero file, una sola chiamata) e una volta con Files.lines come Stream<String> filtrato con un Predicate<String>. L'esempio usa un file temporaneo di sistema in modo da funzionare in qualsiasi sandbox.

java— editable, runs on the server

Cosa ricavare dall'esecuzione:

  • Lo stesso file è stato scritto in quattro modi diversi. Files.writeString è il percorso più breve per "metti questa stringa in questo file"; FileWriter è il writer grezzo classico; BufferedWriter aggiunge un buffer in memoria (utile quando si scrivono molti piccoli blocchi); PrintWriter aggiunge printf. Ognuno ha sovrascritto il contenuto precedente perché la modalità di apertura predefinita è "tronca poi scrivi" — bisogna passare StandardOpenOption.APPEND (trattato nel capitolo sulla scrittura) per aggiungere a un file.
  • Ogni writer è stato eseguito all'interno di try-with-resources. Omettere questo su un writer bufferizzato è il bug in cui gli ultimi caratteri non raggiungono mai il disco — close() è ciò che svuota il buffer finale.
  • Files.readString ha restituito l'intero file come una sola String — adatto per file piccoli, la scelta sbagliata per un log da 4 GB. Files.lines ha restituito uno Stream<String> che puoi passare attraverso filter, map e count senza tenere l'intero file in memoria. Il try-with-resources obbligatorio sullo stream è necessario perché lo stream detiene un file handle aperto.
  • La riga Predicate<String> nameLine = l -> l.startsWith("name") usa lo stesso vocabolario della Parte 12 — un valore Predicate, passato a Stream.filter. Files.lines è dove la API degli stream incontra la API I/O.
  • Files.deleteIfExists è l'eliminazione senza eccezioni: restituisce true se ha rimosso il file, false se non esisteva. Il vecchio File.delete() restituisce un boolean sia per "eliminato" sia per "non è stato possibile eliminare" — Files distingue i due casi lanciando un'eccezione.

Cosa c'è dopo

Prima che la moderna API java.nio.file occupi il resto della parte, il prossimo capitolo tratta la classe che incontrerai per prima in qualsiasi codebase più vecchio: Java File Class. È il tipo legacy per percorsi e metadati — limitato, ma ovunque — e capire cosa non può fare è ciò che motiva l'adozione di Path e Files.

Esercizi

Pratica
Perché `java.io` separa `InputStream`/`OutputStream` da `Reader`/`Writer`?
Perché `java.io` separa `InputStream`/`OutputStream` da `Reader`/`Writer`?
Was this page helpful?