Java gestisce le operazioni di IO servendosi dell’astrazione di flusso o stream e di due classi fondamentali: OutputStream e InputStream.
Stream: sequenza di byte che viaggiano da una origine a una destinazione lungo un canale monodirezionale, di cui
I dati scritti nell’output stream può essere letto dall’input stream.
Java offre molteplici classi per la gestione dell’IO (che compongono la libreria java.io), tutte basate sul concetto fondamentale di flusso o stream.
OutputStream, classe astratta.
InputStream, classe astratta.
Java offre molteplici classi per la gestione dell’IO (che compongono la libreria java.io), tutte basate sul concetto fondamentale di flusso o stream.
OutputStream
rappresenta la classe base della gerarchia di classi che caratterizzano la destinazione dei dati nel flusso.
InputStream
è la classe base della gerarchia di classi che decidono da dove acquisire i dati attraverso il flusso.
Consideriamo come esempio di gestione dell’IO in Java un’applicazione che:
ByteArrayOutputStream
una stringa di caratteri;ByteArrayInputStream
.Dove throws IOException {
rappresenta l'invocazione di metodi che realizzano operazioni di IO e possono sollevare eccezioni di tipo IOException, che vanno gestite.
Con
ByteArrayOutputStream outStream = new ByteArrayOutputStream();
String s = "Questo è un test ...";
for(int i = 0; i < s.length(); i++)
outStream.write(s.charAt(i));
si istanzia un oggetto di tipo ByteArrayOutputStream
e la stringa da utilizzare. Successivamente si realizza l'operazione di scrittura carattere per carattere.
ByteArrayInputStream inStream;
inStream = new ByteArrayInputStream(outStream.toByteArray());
Si istanzia un oggetto di tipo ByteArrayInputStream
, trasmettendo come parametro di ingresso al costruttore l'array di outStream, che rappresenta il buffer da cui estrarre i byte.
int inBytes = inStream.available()
byte inBuf[] = new byte[inBytes];
int bytesRead = inStream.read(inBuf, 0, inBytes);
Si utilizza available() per conoscere quanti caratteri si devono leggere e dimensionare opportunamente un array di byte, che verrà usato nell'operazione di read.
Tra le classi derivate da OutputStream e InputStream troviamo due classi parti-colari che consentono di implementare la tecnica dell’”IO filtrato“: è possibile aggiungere dei filtri ai flussi, per adattarli a specifiche esigenze di programmazione.
FilterOutputStream
: classe astratta base di tutte le classi di flussi di output filtrati, che offre la possibilità di creare un flusso a partire da un altro.
È possibile definire filtri multipli concatenati usando più livelli di annidamento.
FilterInputStream
è complementare a FilterOutputStream
per i flussi di Input filtrati.
I filtri di flussi sono realizzati secondo un pattern che prende il nome di decorator: uso di oggetti annidati per aggiungere in modo dinamico e trasparente responsabilità a singoli oggetti.
Il pattern decorator è molto simile alla composizione: la classe di livello superiore ha come attributo un oggetto della classe di livello inferiore.
La differenza è che la classe di livello superiore deve avere la stessa interfaccia della classe dell’oggetto contenuto, così da poter far uso delle due classi in maniere trasparente al programmatore.
Usando differenti combinazioni di pochi tipi di wrapper è possibile ottenere molti comportamenti diversi. Utilizzando l’ereditarietà è possibile ottenere lo stesso risultato solo estendendo una classe per ogni comportamento
Rispetto all’ereditarietà utilizzare il pattern Decorator implica:
Decoratore
Ereditarietà
DataOutputStream
consente di inserire istanze di tipi primitivi (ad es. int) e String in uno stream di Output.
PrintStream
produce un output formattato.
BufferedOutputStream
viene usato per prevenire una scrittura fisica ogni volta che si scrive su un flusso, i dati in scrittura sono posti su un buffer e l’utente sollecita una scrittura sul flusso con flush().
DataInputStream
e BufferedInputStream
sono i complementari di DataOutputStream e BufferedOutputStream per i flussi di Input.
LineNumberInputStream
registra i numeri di riga nello stream di Input.
PushBackInputStream
dispone di un buffer Push-Back della dimensione di un byte, cosicché l’utente può operare un inserimento nel buffer dell’ultimo byte letto.
Dove con
DataInputStream(fis);
Il costruttore del filtro richiede in ingresso il riferimento di un altro flusso o filtro a cui connettersi. Questo mette in luce la natura dei filtri: sebbene derivate da InputStream o OutputStream, le classi-filtro non sono dei veri e propri stream, infatti hanno come end-point degli stream anziché dei device.
Con le istruzioni
int i= dis.readInt();
String s= dis.readUTF();
Posso fare operazioni di lettura senza l'obbligo di usare array di tipo byte come nel caso precedente.
Ma il filtro consente di ottenere un accesso tipizzato al canale del flusso:
byte inBuf[] = new byte[inBytes];
int bytesRead = inStream.read(inBuf, 0, inBytes);
Infine attraverso
dis.close();
fis.close();
l'ordine di invocazioen dei metodi close() segue all'inverso l'ordine della catena filtri/flussi: ocorre chiudere prima il filtro (o la sua successione) e poi il flusso.
Analogamente, possiamo usare nel seguente modo la combinazione del filtro DataOutputStream e del flusso FileOutputStream.
Mostra codiceNotiamo come in scrittura non siamo limitati solo a scrivere un byte o un array di byte come nel caso precedente outStream.write(s.chartAt(i));
ma possiamo scrivere valori dei tipi primitivi e String:
dis.writeDouble(1.3456);
dis.writeInt(44);
dis.writeUTF("ciao mondo");
L’IO su file è realizzabile per mezzo della combinazione di FileOutputStream
e FileInputStream
. Java dispone di un’altra classe che prende il nome di RandomAccessFile
:
Per facilitare la gestione dell’IO su file, Java offre anche un’ulteriore classe:File
.
Questa classe consente di accedere a uno o più file o directory, e utilizza le convenzioni di assegnazione dei nomi del sistema operativo host.
Dove
File file = new File("myFile.txt");
try {
file.createNewFile();
} catch (IOException x) {
System.err.format("createFile error: %s%n", x.getMessage());}
Crea un file di nome "myFile.txt"
I seguenti comandi invece realizzano operazioni di lettura:
in = new FileInputStream(file);
istanziazione InputStream sul file;BufferedReader reader = new BufferedReader(new InputStreamReader(in));
istanziazione di un BufferReader;while ((line = reader.readLine()) != null)
lettura riga per riga del file;System.err.println(x)
; gestione eventuali eccezioni;if (in != null) in.close();}
chiusura flusso.I seguenti comandi realizzano operazioni di scrittura:
(newFileOutputStream(file));
istanziazione OutputStream sul file;out = new BufferedOutputStream
istanziazione di un BufferedOutputStream;out.write(data, 0, data.length);
scrittura dell'intero array di byte;System.err.println(x);
gestione di eventuali eccezioni;out.close();
chiusura flusso.La libreria java.io in Java 1.1 estende la libreria di con nuove classi, organizzate in due gerarchie (una per l’input e una per l’output) con a capo rispettivamente le classi astratte Reader e Writer.
Non si deve erroneamente pensare che Reader e Writer abbiano soppiantato InputStream e OutputStream. Esistono casi, infatti, dove si possono usare congiuntamente le vecchie classi con le nuove, usando classi “ponte”:
Reader e Writer sono state introdotte per la gestione di caratteri Unicode a 16 bit, mentre le vecchie classi gestivano solo caratteri a 8 bit. Inoltre, le nuove classi sono più performanti.
Java offre una classe di supporto per i flussi chiamata StreamTokenizer (esiste anche un corrispettivo per le stringhe StringTokenizer):
StreamTokenizer definisce quattro metodi costanti:
L’utente può modificare i parametri del tokenizzatore secondo apposite funzioni, si rimanda alla documentazione online della classe per maggiori dettagli.
Istanzio un oggetto BufferedReader, ovvero un filtro che bufferizza un flusso di Input. In ingresso pongo la conversione in Reader di un InputStream, che è quello canonico dalla tastiera. System.out e System.in sono due flussi, rispettivamente di input e di output. Il flusso così ottenuto è passato al costruttore di StreamTokenizer:
BufferedReader inData = new BufferedReader(
new InputStreamReader(System.in));
StreamTokenizer st = new StreamTokenizer(inData);
Segnalo al Tokenizzatore che i caratteri ‘.' e ‘-' non devono essere presi in esame durante l'analisi del flusso:
st.ordinaryChar('.');
st.ordinaryChar('-');
Entro in un ciclo while in cui: determino il token corrente; valuto che tale token è il carattere di fine flusso e se sono al fine flusso esco dal ciclo.
while (st.nextToken() != StreamTokenizer.TT_EOF) {
Valuto all'interno del ciclo switch il tipo di token: carattere di fine riga, numero, parola o ignoto:
String s;
switch(st.ttype) {
case StreamTokenizer.TT_EOL:
s = new String("EOL");
break;
case StreamTokenizer.TT_NUMBER:
s = Double.toString(st.nval);
break;
case StreamTokenizer.TT_WORD:
s = st.sval;
break;
default:
s = String.valueOf((char)st.ttype); }
Prendo dal Tokenizer una rappresentazione numerica del token e lo converto da String a double:
s = Double.toString(st.nval);
Prendo dal Tokenizer una rappresentazione a stringa del token e assegno a s:
s = st.sval;
Prendo dal Tokenizer il valore di tipo di token, opero il casting in char, e lo assegno a un oggetto di tipo String:
s = String.valueOf((char)st.ttype); }
Chiudo i flussi
inStream.close();
inData.close();}}
Gestione delle operazioni di IO in Java. Sono stati trattati i seguenti argomenti:
2. La modellazione a oggetti e il linguaggio UML (richiami)
3. Generalità su Java e la programmazione ad oggetti
6. Regole di traduzione da UML a Java/C++
7. Programmazione multi-thread
8. Sincronizzazione tra thread
9. Programmazione client-server con socket TCP/IP (Java networkin...
10. Programmazione di applicazioni client-server: il Pattern Proxy...
12. Design Patterns
13. Pattern architetturali - Esempi
14. Design pattern creazionali. Esempi
15. Design pattern strutturali. Esempi
16. Introduzione alle tecnologie middleware
17. Modelli di middleware: RPC, MOM, TP, TS
Bruce Eckel, “Thinking in Java” capitolo 12