Input/output in java

Introduzione

I programmi Java effettuano le operazioni di I/O tramite l’uso degli stream (o flussi). Si tratta di un’astrazione che rappresenta una connessione ad un canale di comunicazione, ed è quindi è collegato a un dispositivo fisico del sistema di I/O di Java. Gli stream possono sia leggere da un canale di comunicazione che scrivere su tale canale e si parla di stream di input, e stream di output.

Gli stream forniscono una astrazione del canale nel senso che si comportano nello stesso modo indipendentemente dalle caratteristiche dello specifico dispositivo a cui il canale è collegato. La direzione di comunicazione è sempre univoca, quindi si parla di output stream se lo stream è utilizzato per scrivere sul canale e di input stream se è utilizzato per leggere dal canale. Non esistono, quindi, stream che consentono la lettura e la scrittura sul canale, ma se ve ne è necessità è necessario aprire due stream sullo stesso canale.

Le classi messe a disposizione da java per lo streaming sono incluse nel pacchetto java.io, ed organizzate in una gerarchia molto complessa mostrata nella figura seguente.

java io gerarchia

In generale è possibile fare la distinzione  tra byte oriented stream e char oriented stream.

Nel primo caso l’unità atomica di memorizzazione è il byte e si parla di I/O binario. Viene usato in generale per i dati (es. i bit di un’immagine digitale o di un segnale sonoro digitalizzato). I flussi di byte sono divisi in flussi d’ingresso (InputStream) e flussi d’uscita (OutputStream).

I chart oriented stream sono stati introdotti con la versione 2 della piattaforma java e vengono utilizzati quando deve essere trattare del testo in formato Unicode a 16bit. In questo caso si parla di I/O testuale (es. i caratteri ASCII) e gli stream sono divisi in lettori (Reader) e scrittori (Writer).

Raramente vengono utilizzate le classi base ed i loro metodo per operare sugli stream, questo perché classi specifiche hanno in genere metodi che si adattano meglio alle esigenze del programma. Per farlo utilizzeremo il design pattern Decorato che, in estrema sintesi, consiste nello stratificare più oggetti fino ad ottenere quello con le funzionalità più opportune.

Byte Oriented Stream

Le tipologie di InputStream principali sono:

Classe Funzione
ByteArrayInputStream Consente l’utilizzo di buffer in memoria come InputStream
StringBufferInputStream Converte una stringa in InputStream
FileInputStream Per leggere informazioni da File
PipeInputStream Implementa il concetto di Pipe
SequenceInputStream Converte due o più oggetti InputStream in un solo InputStream
BufferedInputStream Aggiunge funzionalità di bufferizzazione alle operazioni di lettura
DataInputStream Fornisce funzionalità per la lettura da uno stream di tipi primitivi java

Le tipologie di OutputStream principali sono:

Classe Funzione
ByteArrayOutputStream Crea un buffer in memoria, tutti i dati trasmessi nel flusso vengono inseriti in tale buffer.
FileOutputStream Per trasmettere informazioni da File
PipeOutputStream Implementa il concetto di Pipe
BufferedOutputStream Aggiunge funzionalità di bufferizzazione alle operazioni di scrittura
DataOutputStream Fornisce funzionalità per la scrittura su file di tipi primitivi java

Lettura e Scrittura di un File Binario

Per leggere un file binario è sufficiente istanziare un oggetto di tipo FileInputStream, utilizzando il costruttore che accetta il nome del file come parametro. Il seguente esempio legge il proprio file .class e verifica che si tratti di un file java leggendo il magic number.

Nell’esempio lo stream di byte è manipolato in modo diretto. Ne caso si renda necessario avere performance migliori, ad esempio se si devono manipolare file binari di grandi dimensioni, è possibile utilizzare la classe BufferedInputStream che può essere istanziata passando al costruttore l’oggetto FileInputStream (design pattern decorator):

BufferedInputStream bis = new BufferedInputStream( new FileInputStream( ... ) );

In modo analogo è possibile scrivere file binari utilizzando le classi FileOutputStreamBufferedOutputStream.

Proseguendo nel nostro esempio di lettura del magic number, notiamo che la sequenza di 4 byte 0xCAFEBABE può essere rappresentato con un numero intero (in java il tipo primitivo int occupa esattamente 4 byte). Quindi possiamo proporre una implementazione in cui dal file vengono letti valori interi e confrontati col magic number. Per farlo utilizziamo la classe DataInputStream ed il suo metodo readInt().

Accesso alla Memoria

Nella gerarchia del pacchetto java.io abbiamo visto che sono presenti le classi ByteArrayOutputStream e ByteArrayInputStream per l’input e l’output di array di byte che utilizzano buffer in memoria come sorgente e come destinazione dei flussi di input ed output. Ma anche le classi PipedInputStream e PipedOutputStream, che permettono la comunicazione, tramite una pipe in memoria, di due thread di un’applicazione. Questi stream devono sempre essere creati in coppia: un lato della pipe può essere creato senza essere connesso, mentre l’altro sarà sempre creato connettendolo al primo. Vediamo un esempio di uso di una pipe:

Si può notare che la pipe di output è creata senza collegamento ad alcuno stream mentre quella di input è collegata a quella di output passandogliela come parametro nel costruttore. I due stream sono poi decorati utilizzando i data input ed output stream per poter scrivere e leggere direttamente stringhe.

Char Oriented Stream

Le tipologie di Reader principali sono:

StringReader Rappresenta uno stream che opera su stringhe
InputStreamReader E’ una classe bridge tra char stream e byte stream
FileReader Fornisce il supporto alla lettura di caratteri da un file
PipedReader Consente la lettura di caratteri da una pipe
BufferedReader Aggiunge funzionalità di bufferizzazione alla stream in modo che non vi sia necessità di accedere al file system ad ogni operazione di lettura
LineNumberReader Consente di tenere traccia del numero di linea dal quale sono letti i caratteri

Le tipologie di Writer principali son:

StringWriter Consente di scrivere stream manipolati come stringhe
OutputStreamWriter E’ una classe bridge tra char stream e byte stream
FileWriter Fornisce il supporto alla scrittura di caratteri in un file
PipedWriter  Consente la scrittura di caratteri in una pipe
PrintWriter Fornisce supporto alla stampa formattata di caratteri
BufferedWriter Aggiunge funzionalità di bufferizzazione alla stream in modo che non vi sia necessità di accedere al file system ad ogni operazione di scrittura

Lettura e Scrittura di un File di Testo

Per leggere un file di testo su file system è sufficiente utilizzare la classe FileReader, che fornisce di un costruttore a cui è possibile passare direttamente il nome del file.

In questo modo utilizzeremo uno stream non bufferizzato e quindi meno efficiente. Per velocizzare le operazioni di lettura è possibile utilizzare un BufferedReader il cui costruttore accetta un oggetto di tipo FileReader (design pattern decorator). In modo analogo sono utilizzabili le classi FileWriter e BufferedWriter.

Un esempio di programma che legge un file da file system e lo scrive sul video è il seguente:

Stream Predefiniti

Il package java.lang definisce alcuni stream predefiniti contenuti nella classe System. Si tratta di tre variabili statiche e pubbliche denominate in, out e err. Queste si riferiscono rispettivamente allo standard input, che per default è la tastiera, al flusso standard di output, che per default è lo schermo, e al flusso standard di errori che, anche in questo caso, per default è lo schermo.

Per leggere dallo standard input è possibile utilizzare la classe di utilità Scanner: