Prendiamo spunto dalla discussione fatta nello scorso articolo per approfondire il discorso sul funzionamento della lettura e scrittura in java. Come ci ha mostrato l'esempio, le operazioni di lettura/scrittura su un file, su un canale di rete o su un qualsiasi altro supporto che possa essere virtualizzato da un flusso dati in ingresso o in uscita, si effettuano sempre alla medesima maniera.
Il package java.io
è stato strutturato sulla base di due classi, una per l'input, InputStream
, ed una per l'output, OutputStream
, che presentano rispettivamente dei metodi per la lettura (read) e dei metodi per la scrittura (write). In particolare esiste il metodo per la lettura e la scrittura di un singolo byte che è astratto.
Per InputStream:
Public abstract int read();
Per OutputStream:
Public abstract void write(int b);
Sull'estensione di queste classi astratte vengono costruite le classi concrete, relative allo specifico canale che si vuole utilizzare: ci sarà così un FileInputStream
ed un FileOutputStream
, per la lettura / scrittura di file. Tra le altre cose, per poter accedere alla lettura del disco fisso, tali classi devono fare delle chiamate al sistema operativo (per questo motivo fanno delle chiamate al metodo tramite il modificatore d'accesso native).
Fin qui nulla di particolare, se non il fatto che, indipendentemente dal tipo di stream, li utilizzeremo tutti allo stesso modo (astrazione). La particolarità risiede nella presenza di classi accessorie a queste finora citate, i cosiddetti filtri.
Un filtro è un modo per aggiungere funzionalità specifiche alle operazioni di lettura o scrittura. Quindi una classe filtro ha un riferimento al relativo stream sul quale effettua delle operazioni. Senza ledere alla generalità dell'argomento, prendiamo come esempio la classe filtro FilterOutputStream
che ha un riferimento a un'istanza di OutputStream
, out
(stessa cosa per InputStream
). Un filtro concreto, nel metodo write()
, farà delle operazioni e poi richiamerà opportunamente il metodo sull'istanza di OutputStream
.
Listato 1. Un esembio di un filtro
//FilterOutput
Stream.javapublic void write (int b){
//fai qualcosa con b...
//scrivi b sul flusso di output
out.write(b);
}
È evidente che la stessa cosa verrà fatta indipendentemente dall'implementazione concreta di OutputStream, in quanto stiamo lavorando sul supertipo genitore. Quindi riusciremo ad effettuare la stessa operazione su file, su reti, su oggetti, senza conoscere il tipo sottostante.
La cosa più interessante è che FilterOutputStream
(e di complemento FilterInputStream
) estende da OutputStream
, diventando anch'esso polimorficamente un OutputStream
. Ciò significa che, attraverso questo meccanismo, possiamo creare concatenazioni di servizi specifici su un flusso di base, senza conoscere i dettagli implementativi di tale flusso. Ad esempio, potremo scrivere su un file, facendo il buffer e cifrandone il contenuto nel seguente modo:
Listato 2. Scrive su un file, fa il buffer e cifra il contenuto
//in scrittura
FileOutputStream fos = new FileOutputStream(file);
BufferedOutputStream bos = new BufferedOutputStream(fos);
CyperOutputStream cos = new CyperOutputStream(bos);
..//
cos.write(...);
..//
//in lettura
FileInputStream fis = new FileInputStream(file);
BufferedInputStream bis = new BufferedInputStream(fis);
CyperInputStream cis = new CyperInputStream(bis);
..//
int x=cis.read();
..//
A partire da queste considerazioni riprendiamo l'algoritmo utilizzato per lo zip dei file, infatti, ZipOutputStream
non è altro che un filtro erede di FilterOutputStream
. Viene messo in pratica quanto detto, cioè viene applicato l'algoritmo di zip (secondo lo standard) al flusso dati in scrittura.
Tutto l'I/O di java si basa su questo particolare pattern noto come Decorator, attraverso il quale riusciamo a costruire complesse logiche di trasformazione, scomponendo i singoli servizi in filtri. Inoltre, attraverso l'utilizzo di "supertipi", si riescono a gestire in maniera semplice ed elegante, servizi potenzialmente complessi.
Immaginiamo ad esempio di voler salvare un oggetto, istanza di una classe, con relativo stato su file system, e di voler inoltre comprimere il tutto, e perché no, anche cifrare per motivi di riservatezza. Utilizzando quanto abbiamo scritto finora si riesce a fare in maniera molto veloce, pulita e facilmente recuperabile applicando qualche filtro e riutilizzando le classi di base di java (ObjectOutputStream
).