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.
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
public class ClassFileCheck { // Magin Number occupa 4 byte ... per le classi java e' 0xCAFEBABE private static byte[] magicNumber = { (byte) 0xCA, (byte) 0xFE, (byte) 0xBA, (byte) 0xBE }; public static void main(String[] args ) { try { FileInputStream fis = new FileInputStream( "bin/it/inspired/file/ClassFileCheck.class" ) ; // Buffer temporaneo per leggere 4 byte byte[] buffer = new byte[4]; // Legge 4 byte alla volta if ( fis.read( buffer ) != -1 ) { if ( Arrays.equals( magicNumber, buffer ) ) { System.out.println( "Il file e' una classe java valida" ); } else { System.out.println( "Il file non e' una classe java valida" ); } } fis.close(); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } } } |
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 FileOutputStream
e BufferedOutputStream
.
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()
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
public class ClassFileCheck { // Magin Number occupa 4 byte ... per le classi java e' 0xCAFEBABE private static int magicNumber = ((0xCA) << 24) | ((0xFE) << 16) | ((0xBA) << 8) | (0xBE); public static void main(String[] args ) { try { DataInputStream dis = new DataInputStream ( new FileInputStream( "bin/it/inspired/file/ClassFileCheck.class" ) ); // Valore letto dal file int input; // Legge 4 byte alla volta if ( ( input = dis.readInt() ) != -1 ) { if ( magicNumber == input ) { System.out.println( "Il file e' una classe java valida" ); } else { System.out.println( "Il file non e' una classe java valida" ); } } dis.close(); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } } } |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
public class PipedThread extends Thread { protected DataInputStream dis; public PipedThread(InputStream is) { // Decoro la pipe di input con le // funzionalita' di DataOutputStream this.dis = new DataInputStream( is ); } public void run() { String str; try { // Legge fino a quando l'eccezione indica la // terminazione dei dati while (true) { str = dis.readUTF(); System.out.println("Letta: " + str); } } catch (IOException e) { e.printStackTrace(); } } public static void main(String args[]) throws IOException { // Pipe di output e la pipe di input collegata PipedOutputStream oPipe = new PipedOutputStream(); PipedInputStream iPipe = new PipedInputStream( oPipe ); // Decoro la pipe di output con le // funzionalita' di DataOutputStream DataOutputStream dos = new DataOutputStream( oPipe ); // Avvio del trhead ( new PipedThread( iPipe ) ).start(); // Invio il messaggio al thread dos.writeUTF( "Scrivo qualcosa!" ); dos.close(); } } |
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.
1 |
FileReader fileToRead = new FileReader("mio-file.txt"); |
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
.
1 |
BufferedReader fileToRead = new BufferedReader( new FileReader("mio-file.txt") ); |
Un esempio di programma che legge un file da file system e lo scrive sul video è il seguente:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
public class ReadFromFile { public static void main(String[] args ) { try { // Apertura dello stream BufferedReader fileToRead = new BufferedReader( new FileReader( "mio-file.txt" ) ); // Carattere corrente int ch = 0; while ( (ch = fileToRead.read()) != -1 ) { // Il carattere letto e' di tipo intero, // lo convero in char prima di stamparlo System.out.println( (char) ch ); } fileToRead.close(); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } } } |
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
:
1 2 3 4 5 6 7 |
Scanner scanner = new Scanner(System.in); String myString = scanner.next(); int myInt = scanner.nextInt(); scanner.close(); System.out.println("myString is: " + myString); System.out.println("myInt is: " + myInt); |