W3docs

Panoramica di Java NIO

Introduzione a Java NIO e NIO.2 — canali, buffer, selettori e il pacchetto java.nio.file.

I quindici capitoli precedenti riguardavano java.io — stream, Reader/Writer, File, Serializable. Quella API era l'I/O originale di Java ed è ancora ampiamente utilizzata. NIO è la famiglia di API aggiunta successivamente da Java per coprire ciò che java.io non poteva. Si compone di due parti che condividono un prefisso di pacchetto e poco altro:

  • NIO (Java 1.4, 2002) — java.nio.* — canali, buffer, selettori. Una forma diversa per l'I/O: basata su byte-buffer, facoltativamente non bloccante, progettata per server ad alto throughput.
  • NIO.2 (Java 7, 2011) — java.nio.file.* — le classi Path, Files, FileSystem e WatchService. Un sostituto più comodo di java.io.File e una sede per funzionalità del filesystem che java.io non ha mai avuto (link simbolici, attributi estesi, I/O su file asincrono, monitoraggio delle directory).

Stai utilizzando parti di NIO.2 fin dall'inizio di questa sezione: Path, Files.newBufferedReader, Files.newInputStream sono tutti java.nio.file. Questo capitolo fa un passo indietro e mostra dove si inseriscono questi elementi e a cosa serve il resto del pacchetto.

Stream e canale: due forme diverse

InputStream.read() restituisce un byte. OutputStream.write(int) scrive un byte. Il modello mentale è una pipe byte per byte. I decoratori con buffer la rendono veloce, ma l'astrazione è sequenziale e monodirezionale.

Un canale (java.nio.channels.Channel) è bidirezionale, orientato ai byte-buffer e supporta operazioni che InputStream non può esprimere:

  • Lettura e scrittura attraverso un ByteBuffer — non un byte[].
  • Memory-map di una regione di file in RAM e lettura/scrittura come buffer.
  • Scatter di una lettura in più buffer (header → uno, payload → un altro).
  • Gather di una scrittura da più buffer (un singolo write() produce un output contiguo).
  • Contrassegnare un canale come non bloccante e lasciare che un Selector multiplexing migliaia di essi su un singolo thread.

Il compromesso è la verbosità. Il codice con canali legge e scrive attraverso un ByteBuffer con chiamate esplicite a flip() e position(); java.io nasconde tutto ciò dietro read(byte[]). Per la lettura tipica di file, preferisci le API java.io/Files. Passa ai canali quando hai bisogno di una delle funzionalità esclusive dei canali.

// channel-shaped read into a 1 KB buffer
try (FileChannel ch = FileChannel.open(path, StandardOpenOption.READ)) {
  ByteBuffer buf = ByteBuffer.allocate(1024);
  int n = ch.read(buf);                              // fills the buffer; updates position
  buf.flip();                                        // switch to "read what was just written"
  while (buf.hasRemaining()) {
    process(buf.get());
  }
}

Il passaggio flip() è il momento in cui le persone imparano che ByteBuffer ha la propria piccola macchina a stati.

ByteBuffer: position, limit, capacity

Un ByteBuffer è un byte[] di dimensione fissa (o un blocco di memoria off-heap) più tre indici:

  • position — il prossimo byte da leggere o scrivere.
  • limit — l'indice oltre l'ultimo byte che puoi toccare.
  • capacity — la dimensione fissa del buffer; non può cambiare.
0 ─────── position ─────── limit ─────── capacity
   (consumed)   (active region)   (untouchable / empty)

Il buffer si trova convenzionalmente in una di due modalità:

  • Modalità scrittura (predefinita): inserisci byte con put(byte). position avanza; limit == capacity.
  • Modalità lettura: estrai byte con get(). position avanza; limit è dove hai smesso di scrivere.

flip() passa dalla scrittura alla lettura: imposta limit = position (segna dove terminano i dati) e azzera position = 0 (inizia a leggere dall'inizio). clear() torna alla modalità scrittura (position = 0, limit = capacity). Gli errori qui sono la fonte più comune della frustrazione "ho letto zero byte; perché?".

I buffer off-heap (ByteBuffer.allocateDirect(n)) bypassano l'heap della JVM e consentono al sistema operativo di leggerli/scriverli direttamente senza una copia extra. Sono più lenti da allocare, più veloci per le operazioni di I/O, e la scelta giusta solo per il codice di I/O nel percorso critico.

Selettori: un thread, molti canali

Prima dei thread virtuali (Java 21), gestire migliaia di connessioni di rete concorrenti in Java significava o migliaia di thread OS (uno per connessione — costoso) oppure un singolo thread che faceva multiplexing con un Selector:

Selector sel = Selector.open();
serverChannel.register(sel, SelectionKey.OP_ACCEPT);
while (true) {
  sel.select();                                       // blocks until any channel is ready
  for (SelectionKey k : sel.selectedKeys()) {
    if (k.isAcceptable()) accept(k);
    if (k.isReadable())   read(k);
  }
}

Il sistema operativo notifica la JVM quando qualsiasi canale registrato può fare progressi; la JVM ti fornisce il set pronto; esegui una lettura o scrittura non bloccante e torni a select(). Il codice framework sotto Netty, gRPC e Spring WebFlux ha questa forma.

Con i thread virtuali (Thread.startVirtualThread(...)), il più semplice pattern "un thread per richiesta" scala agli stessi conteggi di connessione senza la coreografia del Selector — i thread virtuali si parcheggiano su I/O bloccante essenzialmente senza costi. Per il codice applicativo nuovo su Java 21+, il ciclo del selettore è sempre più una preoccupazione delle librerie; di solito non lo scrivi a mano. Per il codice di libreria e le JVM pre-Loom, è il pattern standard.

java.nio.file: la moderna API per i file

Questa è la metà di NIO che utilizzerai nel codice di tutti i giorni. Sostituisce java.io.File e la maggior parte delle parti relative ai file di java.io:

java.iojava.nio.fileMotivo della sostituzione
FilePathImmutabile, agnostico rispetto all'OS, nessun metodo I/O integrato
File.list()Files.list(Path), Files.walk(Path)Stream<Path>; chiudibile; rispetta i link simbolici
new FileInputStream(...)Files.newInputStream(path)Varianti consapevoli del charset per il testo; un'unica API di apertura coerente
file.delete() che restituisce false in caso di fallimentoFiles.delete(path) che lancia IOExceptionI fallimenti sono visibili, non silenziosi
nessun equivalenteFiles.walkFileTree, WatchService, API per link simbolici, viste degli attributi di fileFunzionalità che java.io non ha mai avuto

I prossimi due capitoli coprono Path e Files in dettaglio. La regola empirica: per il lavoro con file in Java dal 2024 in poi, usa java.nio.file. java.io.File è ancora presente perché il vecchio codice lo utilizza, ma il nuovo codice dovrebbe fare riferimento a Path per impostazione predefinita.

Un esempio pratico: round-trip di un file tramite canale e buffer

Il programma seguente copia un piccolo file di testo nel modo canale-e-buffer per rendere concreti position/limit/flip. Apre il sorgente come FileChannel, legge in un ByteBuffer, fa il flip, scrive su un FileChannel di destinazione e stampa lo stato del buffer a ogni passaggio così puoi vedere come si spostano gli indici.

java— editable, runs on the server

Cosa ricavare dall'esecuzione:

  • Il ciclo ha stampato lo stato del buffer a ogni passaggio. Dopo un read(), position era il numero di byte letti e limit era ancora capacity — questa è la "modalità scrittura": spazio rimasto alla fine. Dopo flip(), position = 0 e limit = il numero appena letto — questa è la "modalità lettura": i byte si trovano tra 0 e limit. I due indici codificano "dove vivono i dati" senza copiarli.
  • Il buffer era di 16 byte; il file era 44. Il ciclo è andato in tre iterazioni: 16, 16, 12. Una volta che il buffer era vuoto (dopo che write lo aveva svuotato), clear() lo ha reimpostato in "modalità scrittura" così il prossimo read() poteva riempirlo di nuovo. Questo è il pattern del canale in miniatura: riempire, flip, svuotare, clear, ripetere.
  • transferTo ha eseguito la stessa copia in una sola riga senza alcun ByteBuffer coinvolto. Su Linux, questo corrisponde a una singola syscall sendfile() — i byte viaggiano da kernel a kernel senza attraversare la JVM. Quando stai spostando dati tra due canali e non hai bisogno di esaminarli, questo è lo strumento giusto.
  • Nota che il file sorgente è stato creato con Files.writeString e la destinazione riletta con Files.readString — entrambi sono one-liner di java.nio.file che nascondono completamente canali e buffer. Il ciclo dettagliato con canali nel mezzo è quello che scriveresti solo quando hai bisogno di accesso diretto al buffer (parsing binario personalizzato, memory mapping, scatter/gather). Per "copiare un file", transferTo o Files.copy è più breve e almeno altrettanto veloce.
  • Il costruttore FileChannel.open(path, OPTION) è il parallelo di Files.newInputStream(path). L'enum StandardOpenOption (READ, WRITE, CREATE, APPEND, TRUNCATE_EXISTING, ...) è ciò che controlla il comportamento di apertura — c'è un solo posto dove guardare. Quell'enum di opzioni di apertura ricorre nel capitolo successivo.

Cosa viene dopo

Questo capitolo ha nominato i pezzi — canali, buffer, selettori, java.nio.file. Il prossimo capitolo, La classe Java Path, approfondisce il più amichevole di questi pezzi — Path — e i metodi (resolve, relativize, normalize) che utilizzerai ogni volta che lavori con un percorso del filesystem.

Esercizio

Pratica
Hai letto 10 byte da un canale in un `ByteBuffer` di capacity 1024. Vuoi scrivere quei 10 byte su un altro canale. Cosa devi fare tra il `read()` e il `write()`?
Hai letto 10 byte da un canale in un `ByteBuffer` di capacity 1024. Vuoi scrivere quei 10 byte su un altro canale. Cosa devi fare tra il `read()` e il `write()`?
Was this page helpful?