Fondamenti sull'Input/Output in Java

Concetti Chiave
  • Java fornisce un sistema di Input/Output (I/O) coerente e flessibile, basato su stream.
  • Gli stream sono un'astrazione che permette di trattare diversi tipi di dispositivi I/O in modo uniforme.
  • Esistono due tipi principali di stream: byte e caratteri, ognuno con le proprie classi e metodi specifici.
  • Gli stream di byte sono utilizzati per dati binari, mentre gli stream di caratteri sono orientati al testo e supportano Unicode.
  • Le classi di stream sono organizzate in gerarchie, con classi astratte come InputStream e OutputStream per gli stream di byte, e Reader e Writer per gli stream di caratteri.
  • Java ha tre stream predefiniti: System.in, System.out, e System.err, che rappresentano rispettivamente l'input standard, l'output standard e l'output di errore standard.
  • Gli stream predefiniti possono essere utilizzati per leggere e scrivere dati dalla console, ma possono anche essere reindirizzati a dispositivi I/O diversi.

L'Input/Output in Java

Nelle precedenti lezioni di questa guida su Java non è stato fatto molto uso dell'I/O nei programmi di esempio.

In effetti, a parte print() e println(), nessuno dei metodi I/O è stato utilizzato in modo significativo. Il motivo è semplice: la maggior parte delle applicazioni reali di Java non sono programmi basati su testo e console.

Piuttosto, sono programmi con interfaccia utente che si basano su uno dei framework di interfaccia utente grafica (GUI) di Java, come ad esempio Swing; oppure sono applicazioni Web o applicazioni lato server.

Sebbene i programmi basati su testo e console siano eccellenti come esempi didattici, non costituiscono, di norma, un uso importante per Java nel mondo reale. Inoltre, il supporto di Java per l'I/O della console è limitato e un po' scomodo da usare—anche in semplici programmi di esempio. L'I/O della console basato su testo semplicemente non è così utile nella programmazione Java del mondo reale.

Nonostante ciò, Java fornisce un supporto forte e flessibile per l'I/O relativamente a file e reti. Il sistema I/O di Java è coerente e consistente. In effetti, una volta compresi i suoi fondamenti, il resto del sistema I/O è facile da padroneggiare. Qui viene presentata una panoramica generale dell'I/O. Ritorneremo sul sottosistema I/O di Java nelle lezioni future.

Il concetto di Stream

I programmi Java lavorano sull'I/O attraverso un'astrazione che prende il nome di Stream o Flusso.

Uno stream è collegato a un dispositivo fisico dal sistema I/O di Java. Tutti gli stream si comportano allo stesso modo, anche se i dispositivi fisici effettivi a cui sono collegati differiscono. Pertanto, le stesse classi e metodi I/O possono essere applicati a diversi tipi di dispositivi. Questo significa che un stream di input può astrarre molti tipi diversi di input: da un file su disco, una tastiera, o un socket di rete. Allo stesso modo, un stream di output può riferirsi alla console, un file su disco, o una connessione di rete. Gli stream sono un modo pulito per gestire input/output senza dover far comprendere a ogni parte del codice la differenza tra una tastiera e una rete, per esempio. Java implementa gli stream all'interno di gerarchie di classi definite nel package java.io.

Consiglio

java.io e java.nio

In realtà, Java ha due sottosistemi I/O: java.io e java.nio.

Il primo fornisce un'API tradizionale per l'I/O basato su stream, mentre il secondo introduce un'API più moderna e flessibile per l'I/O basato su buffer e canali. Infatti, nio sta per New I/O. Il pacchetto java.nio è stato introdotto in Java 1.4 e fornisce un modo più efficiente per gestire l'I/O, specialmente per applicazioni che richiedono alte prestazioni, come i server di rete.

Studieremo in dettaglio java.nio in una serie di prossime lezioni. Per ora, ci concentreremo su java.io.

Stream di byte e Stream di caratteri

Java definisce due tipi di stream I/O: byte e caratteri.

Gli stream di byte forniscono un mezzo conveniente per gestire input e output in cui l'unità minima di informazione sono ottetti di bit, o byte. Gli stream di byte vengono utilizzati, ad esempio, quando si leggono o scrivono dati binari come possono essere immagini, file audio, o file di dati generali. Questi stream non sono orientati ai caratteri e non utilizzano Unicode. Pertanto, non sono adatti per gestire testo in modo diretto.

Gli stream di caratteri forniscono un mezzo conveniente per gestire input e output di caratteri, ossia testo. Utilizzano Unicode e, pertanto, possono essere internazionalizzati. Inoltre, in alcuni casi, gli stream di caratteri sono più efficienti degli stream di byte.

La versione originale di Java (Java 1.0) non includeva gli stream di caratteri e, quindi, tutto l'I/O era orientato ai byte. Gli stream di caratteri sono stati aggiunti da Java 1.1, e certe classi e metodi orientati ai byte sono stati deprecati. Sebbene il vecchio codice che non utilizza gli stream di caratteri stia diventando sempre più raro, può ancora essere incontrato di tanto in tanto. Come regola generale, il vecchio codice dovrebbe essere aggiornato per sfruttare gli stream di caratteri dove appropriato.

Un altro punto: al livello più basso, tutto l'I/O è ancora orientato ai byte. Gli stream basati sui caratteri forniscono semplicemente un mezzo conveniente ed efficiente per gestire i caratteri, ossia un'astrazione costruita sopra gli stream di byte. In altre parole, gli stream di caratteri sono implementati utilizzando gli stream di byte.

Nel resto di questa lezione vedremo una panoramica sia delle classi di stream di byte che delle classi di stream di caratteri.

Le Classi di Stream di Byte

Gli stream di byte sono definiti utilizzando due gerarchie di classi.

Al vertice ci sono due classi astratte: InputStream e OutputStream.

Ognuna di queste classi astratte ha diverse sottoclassi concrete che gestiscono le differenze tra vari dispositivi, come file su disco, connessioni di rete e persino buffer di memoria. Le classi di stream di byte non deprecate in java.io sono mostrate nella Tabella che segue. Alcune di queste classi sono discusse più avanti in questa lezione. Altre saranno affrontate in una serie di lezioni successive.

Bisogna ricordare, in ogni caso, che per usare le classi di stream, si deve importare java.io.

Classe di Stream Descrizione
BufferedInputStream Stream di input bufferizzato
BufferedOutputStream Stream di output bufferizzato
ByteArrayInputStream Stream di input che legge da un array di byte
ByteArrayOutputStream Stream di output che scrive su un array di byte
DataInputStream Uno stream di input che contiene metodi per leggere i tipi di dati standard di Java
DataOutputStream Uno stream di output che contiene metodi per scrivere i tipi di dati standard di Java
FileInputStream Stream di input che legge da un file
FileOutputStream Stream di output che scrive su un file
FilterInputStream Implementa InputStream
FilterOutputStream Implementa OutputStream
InputStream Classe astratta che descrive l'input di stream
ObjectInputStream Stream di input per oggetti
ObjectOutputStream Stream di output per oggetti
OutputStream Classe astratta che descrive l'output di stream
PipedInputStream Pipe di input
PipedOutputStream Pipe di output
PrintStream Stream di output che contiene print() e println()
PushbackInputStream Stream di input che permette ai byte di essere restituiti allo stream di input
SequenceInputStream Stream di input che è una combinazione di due o più stream di input che saranno letti sequenzialmente, uno dopo l'altro
Tabella 1: Le Classi di Stream di Byte Non Deprecate in java.io

Le classi astratte InputStream e OutputStream definiscono diversi metodi chiave che le altre classi di stream implementano. Due dei più importanti sono read() e write(), che, rispettivamente, leggono e scrivono byte di dati. Ognuno ha una forma che è astratta e deve essere sovrascritta dalle classi di stream derivate.

Le Classi di Stream di Caratteri

Gli stream di caratteri sono definiti utilizzando due gerarchie di classi. In cima ci sono due classi astratte: Reader e Writer. Queste classi astratte gestiscono stream di caratteri Unicode. Java ha diverse sottoclassi concrete di ciascuna di queste. Le classi di stream di caratteri in java.io sono mostrate nella Tabella che segue:

Classe Stream Significato
BufferedReader Stream di caratteri di input bufferizzato
BufferedWriter Stream di caratteri di output bufferizzato
CharArrayReader Stream di input che legge da un array di caratteri
CharArrayWriter Stream di output che scrive su un array di caratteri
FileReader Stream di input che legge da un file
FileWriter Stream di output che scrive su un file
FilterReader Reader filtrato
FilterWriter Writer filtrato
InputStreamReader Stream di input che traduce byte in caratteri
LineNumberReader Stream di input che conta le righe
OutputStreamWriter Stream di output che traduce caratteri in byte
PipedReader Pipe di input
PipedWriter Pipe di output
PrintWriter Stream di output che contiene print() e println()
PushbackReader Stream di input che permette ai caratteri di essere restituiti allo stream di input
Reader Classe astratta che descrive l'input di stream di caratteri
StringReader Stream di input che legge da una stringa
StringWriter Stream di output che scrive su una stringa
Writer Classe astratta che descrive l'output di stream di caratteri
Tabella 2: Le Classi I/O di Stream di Caratteri in java.io

Le classi astratte Reader e Writer definiscono diversi metodi chiave che le altre classi stream implementano. Due dei metodi più importanti sono read() e write(), che leggono e scrivono caratteri di dati, rispettivamente. Ciascuno ha una forma che è astratta e i loro metodi devono essere sovrascritti dalle classi stream derivate.

Gli Stream Predefiniti

Come è noto, tutti i programmi Java importano automaticamente il pacchetto java.lang. Questo pacchetto definisce una classe chiamata System, che incapsula diversi aspetti dell'ambiente di esecuzione.

Ad esempio, utilizzando alcuni dei suoi metodi, si può ottenere l'ora corrente e le impostazioni di varie proprietà associate al sistema. System contiene anche tre variabili di stream predefinite: in, out, e err. Questi campi sono dichiarati come public, static, e final all'interno di System. Questo significa che possono essere utilizzati da qualsiasi altra parte di un programma e senza riferimento a un oggetto System specifico.

System.out si riferisce al flusso di output standard. Per impostazione predefinita, questo oggetto rappresenta la console. System.in si riferisce all'input standard, che è la tastiera per impostazione predefinita. System.err si riferisce al flusso di errore standard, che è anche la console per impostazione predefinita. Tuttavia, questi stream possono essere reindirizzati a qualsiasi dispositivo I/O compatibile.

System.in è un oggetto di tipo InputStream; System.out e System.err sono oggetti di tipo PrintStream. Questi sono stream di byte, anche se sono tipicamente utilizzati per leggere e scrivere caratteri da e verso la console. Come vedremo, si possono racchiudere questi all'interno di stream basati su caratteri, se desiderato.

Nelle lezioni precedenti abbiamo utilizzato System.out nei vari esempi. Si può utilizzare System.err più o meno allo stesso modo. Come vedremo nelle prossime lezioni, l'uso di System.in è un po' più complicato.