MokaPackages 
I packages del JDK
java.io
di
Piergiuseppe
Spinelli
Uno dei package fondamentali del JDK

 



Ecco un  nuova rubrica dedicata ai nuovi programmatori che si avvicinano a Java, una serie di articoli vogliamo dare una panoramica sulle classi standard del JDK 1.2 (ribattezzata Java 2). Faremo riferimento solo alle calssi più importanti della versione 1.2, evitando di soffermarci sulle classi ed i metodi dichiarati obsoleti e mantenuti per compatibilità con le precedenti versioni


Introduzione

Panta rei, tutto scorre, tutto cambia velocemente e, si sà, mai tanto velocemente come nel mondo dei byte.
Oggi gli utenti finali stanno diventando sempre più smaliziati e mi accade spesso di essere intercettato da qualche conoscente che, identificandomi come uno del mestiere, mi spara addosso raffiche di domande sugli aspetti più reconditi di Windows '98 (che non ho mai usato) o su usi perversi di utility dai nomi tanto esotici quanto sconosciuti. Solitamente, una volta realizzata la mia totale estraneità e stupefatta ignoranza sugli argomenti dell'interrogazione, questi ex-amici si allontanano rapidamente con espressioni che variano dal disprezzo alla compassione mentre io sento l'eco dei loro pensieri: "Allora dobbiamo davvero temere per l'anno 2000, vista l'ignoranza di chi programma i computer!".
Così a me resta la nostalgia di quando l'utente più scaltro aveva una visione dei programmi che può essere così riassunta: entra qualcosa, succede qualcosa che non è dato conoscere, alla fine esce un risultato. E, a ben pensare, la struttura dei programmi dell'epoca (B.M. = Before Macintosh) non era lontana dall'idea della gente comune. Si prendevano dei dati in ingresso, si cercavano altri dati negli archivi di massa, si svolgevano varie elaborazioni e, alla fine, si riscriveva qualcosa, sempre dentro archivi o su stampanti, monitor (anche terminali, se ricordo bene...).
In altre parole, l'I/O (Input/Output = Ingresso/Uscita di dati), era uno dei cardini della programmazione e, naturalmente, l'aspetto più evidente e soggetto alle aspettative ed ai controlli degli utenti.
Nove programmi C su dieci iniziavano con #include stdio.h, e da quel momento si poteva comunicare con il mondo. Poi, come sempre accade, le cose cambiarono: cinque programmi C su dieci cominciavano con #include windows.h, gli accessi ai file venivano sostituiti sempre più spesso da istruzioni di manipolazione di dati gestiti da database, l'interazione tra operatore e macchina richiedeva sempre meno tasti e sempre più click sul muso di strani roditori da tavolo.
Sembrava così avviato l'ineluttabile declino delle librerie di I/O quando, altrettanto repentinamente, avvengono ben due fatti, esplosivi nel manifestarsi ma, in realtà, entrambi frutto di lunghe gestazioni:

    INTERNET e affini: "il computer è la rete!", la rete è fatta da sistemi che comunicano, i sistemi sono fatti di programmi e, dal punto di vista dei programmi, la rete non è altro che I/O.
    Si impongono i paradigmi ad oggetti e, questi, non hanno una corrispondenza naturale ne con i vecchi archivi ne con i moderni RDBMS. Nasce l'esigenza di nuovi meccanismi per la persistenza degli oggetti e, in attessa di una maggiore diffusione e standardizzazione degli OODBMS e considerando il calo verticale del costo della RAM, molte applicazioni hanno tratto profitto da una tecnica che consiste nel lavorare con strutture di oggetti totalmente contenute in memoria per poi congelarle al termine dell'esecuzione in modo da ripristinare lo stato della memoria al prossimo lancio. Questa tecnica di memorizzazione in blocco di un intero albero di oggetti è detta serializzazione e, anch'essa, è un meccanismo di I/O che, tra l'altro, è anche la base del meccanismo di esecuzione di oggetti remoti di JAVA (RMI).
Bene, ecco che in piena era di oggetti e interfacce grafiche, il package java.io, erede della stdio.h del C, è ancora una delle librerie fondamentali, da conoscere e maneggiare con competenza. Diamole uno sguardo insieme.

Java.io
La filosofia adottata da Java per l'I/O è la stessa affermatasi con il C Language ed il C++, si instaura una comunicazione con un altro sistema in modo dipendente dal tipo di sistema in questione e, una volta on-line, si utilizza un unico paradigma per inviare e ricevere dati, dimenticandosi (entro certi limiti) se il nostro interlocutore è una stampante, un disco rigido o un task sul nostro computer o su un sistema remoto.
Ovviamente esistono alcune limitazioni; ad esempio è possibile riposizionarsi su uno stream aperto su un file ma non su console o su un sochet in rete. Resta comunque il fatto che, sommo esempio di polimorfismo, si leggono e scrivono dati usando oggetti che presentano quasi completamente gli stessi metodi, indipendentemente dal fatto che, in effetti, stiamo scrivendo su carta, su memoria magnetica o spedendo dati dall'altra parte del globo.

I/O di byte
Gli stream Java trattano flussi di dati binari, dove l'unità minima di informazione è il byte (che, come vedremo, non corrisponde affatto al carattere).
InputStream e OutputStream sono le classi astratte da cui deriva ogni altra classe per la comunicazione binaria, comprese quelle per scambiare dati primitivi e oggetti. Ogni classe derivata da InputStream deve, come minimo, implementare una versione concreta del metodo public abstract int read() che, a dispetto del tipo ritornato int, restituisce il prossimo byte (0...255) disponibile nello stream o -1 se la lettura è giunta al termine dello stream. Il metodo read() sospende il thread chiamante fino a che un nuovo carattere sia disponibile o lo stream venga terminato, regolarmente o con un'eccezione. Le classi derivate da OutputStream devono almeno implementare il metodo public abstract void write(int b) che utilizza il solo byte meno significativo dell'argomento intero. Sono disponibili metodi per leggere e scrivere interi array di byte o parte di essi. E' possibile forzare lo svuotamento del buffer di output con flush() e saltare caratteri in input con skip(). Per InputStream che supportano il marcamento, controllabile con markSupported(),è possibile eseguire un mark() sulla posizione corrente, leggere o saltare in avanti e, successivamente, ritornare nella posizione marcata cone reset().
PrintStream scrive in come testo formattato il contenuto di variabili di tipo primitivo oppure di reference di tipo String o Object; per Object e le sue classi derivate (cioe tutte!), la formattazione avviene chiamando il metodo toString(). PrintStream è mantebuta per compatibilità con le versione precedenti del JDK e può, eventualmente, essere ancora utilizzata a scopo di debug; in tutti gli altri casi deve essere sostituita con la classe PrintWriter.
I/O di caratteri
Reader e Writer sono classi astratte alla radice di tutte le classi che eseguono I/O su stream di caratteri. Le classi derivate devono implementare, come minimo, i metodi close(), read(char[], int, int) (per Reader) e write(char[], int, int) e flush() (per Writer). E' possibile comunque ridefinire anche altri metodi a scopo di ottimizzazione. Facciano attenzione i neofiti provenienti dal linguaggio C, dove si producono stringhe con print e dati binari con write. In Java vigono convenzioni diverse, dipendenti dalla particolare classe: OutputStream.print(int), ad esempio, scrive un intero in formato binario mentre Writer.write(int) interpreta il parametro intero come codice di un carattere (scartando i 16 bit più significativi); PrintWriter.print(int) e PrintStream.print(int) stampano la stringa formattata rappresentante il valore decimale dell'intero; infine PrintWriter.write(int) si comporta come Writer.write(int) mentre PrintStream.write(int) stampa un byte senza convertirlo secondo la tabella dei codici della piattaforma ospite (uno dei motivi per preferire PrintWriter). Confusi?! Effetti collaterali del polimorfismo (almeno quando viene usato in questo modo...).!
PrintWriter scrive in modo formattato su uno stream di testo. Diversamente da PrintStream implementa la funzionalità di autoflushing scrivendo fisicamente il buffer solo con il metodo println(), implementando il fine linea con il carattere utilizzato dalla particolare piattaforma ospite.
Se qualche lettore viene da UNIX e dal linguaggio C sarà abituato a considerare il carattere come l'unità di misura di ogni tipo di dato e, dicendo carattere, si sottointende un byte con il bit più significativo non sempre preso in considerazione. Per chi, invece, proviene da MS-DOS e dai suoi nipoti e pronipoti a finestre, è già più naturale fare distinzioni fra stream a caratteri e stream binari. Gli adepti di NT, poi, sanno già tutto di UNICODE ed altro.
E' bene chiarire il concetto di carattere in Java. In Java esiste un'unica codifica dei caratteri, quella UNICODE; e questo e vero non solo per il contenuto delle stringhe ma anche per i nomi degli identificatori usati nel linguaggio (variabili, metodi etc...). L'UNICODE è una codifica nata per supportare diversi linguaggi umani e, utilizzando 16 bit, può rappresentare fino a 65536 caratteri di cui (le mie fonti in merito risalgono al '97) 34.168 già assegnati con i primi 128 che corrispondono alla codifica ASCII. Lo standard di trasmissione di stream UNICODE utilizza il formato UTF-8; Java ne usa una versione leggermente modificata:

    il carattere null ('\u0000') è rappresentato da due byte
    sono utilizzate solo le rappresentazioni a 1, 2 o 3 byte.
Per il resto il funzionamento è quello della normale codifica UTF-8:
 
Caratteri da '\u0001' a '\u007f': un byte nella forma:
BYTE1 0 b0 b1 b3 b4 b5 b6 b7
Carattere null ('\u0000') e caratteri da '\u0080' a '\u07ff': due byte nella forma:
BYTE1 1 1 0 b6 b7 b8 b9 b10
BYTE2 1 0 b0 b1 b2 b3 b4 b5
Caratteri da '\u0800' a '\uffff': tre byte nella forma:
BYTE1 1 1 1 0 b12 b13 b14 b15
BYTE2 1 0 b6 b7 b8 b9 b10 b11
BYTE3 1 0 b0 b1 b2 b3 b4 b5
Una stringa di caratteri UTF-8 viene memorizzata in uno stream (ad esempio con il metodo writeUTF) nel seguente modo:
    Primi due byte (nel formato usato da writeShort) sono la lunghezza (in byte effettivi, non in caratteri) della stringa codificata nel seguito.
    Lista di caratteri in codifica UTF (da un minimo di uno ad un massimo di tre byte a carattere)
Utilizzando la codifica UTF-8, si ha la certezza che un file di testo utilizzante caratteri nel solo sottoinsieme ASCII non occuperà più spazio di un normale file di testo con caratteri di un byte (i due byte iniziali per la lunghezza della riga compensano i due terminatori \r\n), mentre resta possibile, ove necessario, rappresentare ogni altro tipo di carattere codificato.

Traduzioni
InputStreamReader e OutputStreamWriter fanno da ponte tra gli stream di byte e quelli di caratteri. Nella traduzione applicano la codifica passata al costruttore o, per default, quella della piattaforma ospite. Dal momento che, per vari motivi (come ad esempio nei linguaggi asiatici), un carattere può essere codificato con più byte ed allo scopo di evitare una chiamata alla routine di traduzione per ogni singolo elemento, gli stream di caratteri vengono spesso bufferizzati nelle applicazioni pratiche:

BufferedReader in = new BufferedReader(new InputStreamReader(System.in)); 
Writer out = new BufferedWriter(new OutputStreamWriter(System.out));
I/O degli altri tipi elementari di dato
DataInput e DataOutput sono le interfacce da cui derivano tutte le classi per l'I/O di dati primitivi (dove la classe String è trattata come tipo primitivo). Attenzione che, per quanto tutti i metodi inizianti con read sono seguiti dal nome di un tipo con la lettera maiuscola, sono ritornati valori primitivi e non Oggetti del rispettivo tipo Warapper: ad esempio readFloat restituisce un float e non un Float.
DataInputStream e DataOutputStream sono classi derivate dagli stream astratti (più esattamente da quelli filtrati che vedremo tra poco) e che implementano, rispettivamente, le interfacce DataInput e DataOutput.
 
 
 

Metodi di DataInput Metodi corrispondente in DataOutput
Metodo Byte in ingresso Tipo in Uscita Note  
readBoolean() 1 boolean false se il byte è 0, altrimenti true writeBoolean(boolean v) 
readByte() 1 byte   write(int b), writeByte(int v)
 readChar() 2 char i due byte (b1, b2) sono interpretati come un carattere unicode con il primo byte letto come più significativo writeChar(int v) , writeChars(String s) 
readDouble() 8 double Legge un long (come readLong) e lo converte in double con Double.longBitsToDouble writeDouble(double v) 
 readFloat()  4 float Legge un int (come readInt) e lo converte in float con Float.intBitsToFloat writeFloat(float v) 
 readFully(byte[] b)  b.length void Riempe l'array b leggendo un numero di byte pari alla sua lunghezza. Se lo stream in input termina prima viene sollevata una EOFException e solo una parte dell'array può essere stata modificata. write(byte[] b) 
readFully(byte[] b, int off, int len) len void Riempe la sezione dell'array b che comincia dall'elemento off leggendo un numero di byte pari a len. Se lo stream in input termina prima viene sollevata una EOFException e solo una parte dell'array può essere stata modificata. write(byte[] b) 
readInt() 4 int Costruisce un intero dai quattro byte letti utilizzando il primo come byte più significativo writeInt(int v) 
readLine() X String Restituisce una stringa fino alla prima coppia \r\n incontrata (scartandola) o alla fine del file. Utilizza ogni byte in ingresso come un carattere ASCII. Non esegue alcuna traduzione e non supporta l'unicode. writeBytes(String s) (non corrisponde esattamente: \r\n devono essere alla fine della stringa)
readLong()  8 long Costruisce un intero lungo dagli otto byte letti utilizzando il primo come byte più significativo writeLong(long v) 
readShort() 2 short Costruisce un intero corto dai due byte letti utilizzando il primo come byte più significativo writeShort(int v) 
readUnsignedByte()  1 int Usa il byte letto come meno significativo nell'intero restituito estendendone il valore a sinistra con degli zeri. L'intero ha un range di valori da 0 a 255. write(int b) (con b opportunamente controllato)
 readUnsignedShort() 2 int Usa un short (come in readShort) è lo utilizza come parte meno significativa nell'intero restituito estendendone il valore a sinistra con degli zeri. L'intero ha un range di valori da 0 a 65535. writeShort(int v) (con v opportunamente controllato)
readUTF() X String Legge una stringa codificata in UTF modificato. writeUTF(String str) 
skipBytes(int n) n int Scarta n byte dallo stream in input  

I/O di oggetti e serializzazione
L'argomento della serializzazione meriterebbe una trattazione separata, sia per la sua complessità che per le tecniche che da essa derivano. Basti pensare che tutta l'evoluzione più recente di Java volge verso l'elaborazione distribuita e che il meccanismo di base per l'esecuzione di oggetti remoti, RMI, si basa sulla serializzazione.
Prima di tutto bisogna capire che la serializzazione è un meccanismo fondante del linguaggio, e questo lo si intuisce, ad esempio, dal fatto che esistono parole chiave, come transient, che sono state concepite proprio per controllare il modo in cui gli oggetti vengono serializzati. La guida in Inglese sulla serializzazione inclusa del JDK 1.2, stampata nella versione PDF, è di ben 66 pagine senza contenere un reference alle API. E' ovvio quindi che, in questa sede, accenneremo solo ai modi d'uso più elementari (ma anche più frequenti) rimandando alla documentazione SUN chi volesse approfondire questo argomento che presenta alcuni aspetti alquanto ostici.
Registrare su uno stream lo stato di un oggetto, ovvero seriarizzarlo, può essere fatto con poche righe Java, come nell'esempio

FileOutputStream tempFile = new FileOutputStream(tempDir + 
File.pathSeparator + "tempFile.$$$");
ObjectOutput oos = new ObjectOutputStream(tempFile);
oos.writeObject(mioOggetto);
oos.flush();
oos.close();
tempFile.close();
La classe di mioOggetto deve implementare l'interfaccia Serializable (oppure, per una gestione più personalizzata della serializzazione, l'interfaccia Externalizable che però non approfondiremo). Serializable è un'interfaccia priva di metodi; essa serve da marcatore per le classi che implementano ObjectOutput per sapere se l'oggetto sia o meno serializzabile.
Lo stato di un oggetto è formato dalla sua classe e relativa versione, da campi di tipo primitivo e da campi reference ad altri oggetti. Serializzare un oggetto, quindi, significa salvare tutto l'albero di oggetti da esso riferiti più o meno direttamente. Dal momento che nulla impedisce ad due oggetti di farsi reciprocamente riferimento, il meccanismo di serializzazione deve evitare di cadere in una ricorsione infinita.
ObjectOutputStream, che è l'implementazione di default per ObjectOutput, utilizza i metodi standard di DataOutputStream per scrivere i campi di tipo primitivo mentre utilizza la classe annidata ObjectOutputStream.PutField per percorrere ricorsivamente gli oggetti a riferiti. Per ogni oggetto registrato viene salvato un header; se percorrendo ulteriormente le ramificazioni dei riferimenti dell'oggetto (o di altri oggetti serializzati sullo stesso ObjectOutput) viene rinvenuto un nuovo puntatore allo stesso oggetto, la ricorsione si interrompe evitando un possibile ciclo infinito e risparmiando spazio nello stream. Questo comportamento, comunque, deve farci riflettere sui limiti della serializzazione come meccanismo di comunicazione, ad esempio su un ObjectOutputStream aperto su un Socket. Ammettiamo di voler trasmettere eventi al sistema remoto tramite un oggetto di tipo MioEvento che contiene un campo pubblico tipoEvento. Potremmo pensare di scrivere il seguente codice:
 
MioEvento ev=new MioEvento();
while(true){
        String tv=AspettaEvento();
        ev.tipoEvento=tv;
        oo.writeObject(ev);
}


Non deve sorprendere (come successe a me quando provai...) che il povero thread lettore riceva sempre lo stesso evento, indipendentemente dai valori successivi assunti da tipoEvento: una volta memorizzato un header di un oggetto serializzabile, la classe ObjectOutput non verifica ulteriormente il contenuto dello stesso le volte successive che si imbatte in esso; ecco perché la modifica del campo tipoEvento resta trasparente durante le successive scritture. Si potrebbe riscrivere il codice come segue:
 

MioEvento ev;
while(true){
        String tv=AspettaEvento();
        ev = new Evento();
        ev.tipoEvento=tv;
        oo.writeObject(ev);
}
ma, in definitiva, la serializzazione non è il metodo migliore per trasmettere lunghi flussi di dati con valori dinamici per comunicare con altri sistemi (a meno di farlo in un framework ben definito come RMI).
La semplice lettura di un oggetto serializzato su uno stream è del tutto simmetrica alla scrittura:
 
FileInputStream tempFile = new FileInputStream(tempDir + 
        File.pathSeparator + "tempFile.$$$");
ObjectInput ois = new ObjectInputStream(tempFile);
MiaClasse mioOggetto=(MiaClasse)ois.readObject();
ois.close();
tempFile.close();


Come si è detto, una classe deve essere dichiarata serializzabile implementando l'omonima interfaccia. Siccome la verità non risiede quasi mai negli estemi (un po' di Buddismo non guasta anche in informatica), accade di frequente che una classe desideri far serializzare solo una parte dei propri campi e, magari, voglia proteggerne altri da occhi indiscreti, oppure forzare un determinato formato di memorizzazione, ad esempio per encriptare dati riservati o aggiungere informazioni utili in lettura per verificare la correttezza dei dati o la loro provenienza (magari con una check-sum o una firma elettronica). Così è possibile dichiarare un campo private transient per evitarne la serializzazione o per eseguirla in modo custom:
 

Class Custom implements Serializable {
        private transient Object segreto;       //Non viene memorizzato
        private transient String password;      //Non viene memorizzato automaticamente
        private Object persistente;             //Viene memorizzato automaticamente
        ....
        private void writeObject(ObjectOutputStream oos){
                //Serializza tutti i campi non transient e non statici
                oos.defaultWriteObject(); 
                String xp = cripta(password);
                //Scrive la password criptata
                oos.writeUTF(xp); 
        }

        private void readObject(ObjectInputStream ois){
                //Deserializza tutti i campi non transient e non statici
                oos.defaultReadObject(); 
                String xp = ois.readUTF();      //Legge la password criptata
                password = decripta(xp);
        }
}


Non ci soffermiamo sul fatto che writeObject e readObject siano metodi privati pur venendo richiamati dalle classi ObjectOuputStream e ObjectInputStream: evidentemente la JVM ci mette un po' di magia personale.
Java è un linguaggio nato in un'epoca dove la sicurezza è un imperativo. Eppure quando si effettua la serializzazione di un oggetto su un file si compie, per così dire, la pubblicazione di tutti i suoi campi non statici, anche protetti e privati se non transienti. Inoltre, al momento della deserializzazione, non è semplice accertarsi che lo stream che viene letto, proveniente da un file rimasto esposto a tutte le intemperie di un qualsiasi sitema operativo esterno alla JVM, abbia il contenuto originale, non corrotto ne falsificato. Per dare una mano alle classi nella verifica dei dati letti, il JDK fornisce l'interfaccia ObjectInputValidation con l'unico metodo:

public void validateObject() throws InvalidObjectException ;

che viene richiamata dall ObjectInputStream appena terminata l'intera lettura ma subito prima di restituire l'oggetto al chiamante.
Queste sono le basi. Il JDK 1.2 estende la serializzazione con meccanismi di sostituzione di oggetti, di verifica di versioni delle classi (un problema è che lo scrivente ed il lettore potrebbero girare su versioni diverse del JDK o utilizzare versioni differenti di package di terze parti) e con altre cose più esoteriche. E' solo una mia idea personale, ma credo che quando un programmatore sente l'esigenza di approfondire temi come serializzazione, introspezione, grana della sincronizzazione, Reference Objects etc..., può ritenere di aver guadagnato la cintura nera in Java e può procedere alla conquista dei successivi Dan.
 

Bufferizzazione
BufferedInputStream e BufferedOutputStream, ed i corrispettivi BufferedReader e BufferedWriter, consentono di utilizzare uno stream esistente in modo bufferizzato. In altre parole il sistema si prende cura di eseguire letture o scritture per pezzi di dati maggiori di quelli corrispondenti alla grana dei singoli metodi di I/O in modo da ottimizzare i tempi di accesso alle periferiche. Derivando dalle rispettive classi filtro, una classe bufferizzata viene sempre costruita su uno stream già esistente e conserva le caratteristiche di quert'ultimo. Alcuni esempi:

BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
BufferedReader in = new BufferedReader(new FileReader("c:\testo"));
BufferedOutputStream in = new BufferedOutputStream(System.out);
Gestione del file system
La classe File rappresenta un file o una directory identificato da un path internamente al file system della macchina ospite secondo le convenzioni di quest'ultima. Sono importanti alcuni campi statici: pathSeparator, pathSeparatorChar, separator e separatorChar; tali campi dovrebbero sempre essere usati quando si costruisce una stringa rappresentante un path piuttosto di usare, ad esempio, "/" o "\\", onde garantire che il codici venga eseguito correttamente su piattaforme diverse.
La classe file permette di svolgere tutte le normali manipolazioni su file e directory. Ecco i metodi più utili:
 

canRead, canWrite Testano i permessi di lettura scrittura
isAbsolute, isDirectory, isFile Testano se si tratta di un path assolluto, di una directory o di un normale file
createTempFile Crea un file temporaneo sulla base di un pattern o di un prefisso dato
exists Testa l'esistenza del file
getName, getParent, getPath Ritornano rispettivamente il nome del file, la radice ed il percorso (p.e., nel caso di "c:\dir\subdir\dati.txt", il file è "dati.txt", il path è "c:\dir\subdir\" e la radice è "c:\"
getAbsolutePath, getCanonicalPath Ritornano il percorso assoluto e quello nella forma canonica dipendente dal sistema ospite
length, lastModified Ritornano la lunghezza e la data di ultima modifica del file
delete, deleteOnExit rimuovono il file, immediatamente o all'uscita della JVM
renameTo Cambia il nome del file
mkdir, mkdirs Creano una nuova directory o un intero ramo di directory
list Ritorna un'array di stringhe con i nomi di file nella directory, eventualmente filtrati con un FilenameFilter.
FileDescriptor rappresenta un file aperto. Il metodo sync forza lo svuotamento dei buffer fisici di I/O (quelli ad alto livello, gestiti dalle classi, devono essere preventivamente svuotati con flush.
FileInputStream e FileOutputStream, ed i corrispettivi FileReader e FileWriter, rappresentano stream aperti su file mentre RandomAccessFile implementa contemporaneamente le interfacce DataInput e DataOutput e, con il metodo seek(long pos), consente di posizionarsi ad una specifica posizione del file aperto per leggere o scrivere.
FilenameFilter serve per filtrare un insieme di file in base ad un pattern. Ad esempio FileDialog di AWT utilizza questa interfaccia per consentire lo scorrimento dei soli file con, per esempio, una determinata estensione.
FilePermission consente di creare permessi di accesso (lettura, scrittura, esecuzione e cancellazione) su file e directory in modo da utilizzarli nel sistema di sicurezza del jdk1.2 per consentire alle classi caricate dinamicamente dal nostro programma di accedere solo ai file che vogliamo e nei limiti da noi impostati. Esempio:

perm = new java.io.FilePermission("/tmp/abc", "read");

I/O su stringhe ed array
L'I/O di Java è così generalizzato (come quello di UNIX, del resto) da consentire di utlizzare la memoria stessa come device. Sono supportati tre formati:

Filtri
Gli stream filtrati sono tutte quelle classi che vengono costruite sulla base di uno stream già aperto per estenderne alcune caratteristiche. Le classi FilterInputStream e FilterOutputStream sono alla base di tutte le seguenti classi concrete:
 
FilterInputStream BufferedInputStream, CheckedInputStream, DataInputStream, DigestInputStream, InflaterInputStream, LineNumberInputStream, ProgressMonitorInputStream, PushbackInputStream
FilterOutputStream BufferedOutputStream, CheckedOutputStream, DataOutputStream, DeflaterOutputStream, DigestOutputStream, PrintStream
Le classi astratte FilterReader e FilterWriter sono meno importanti in quanto la maggior parte degli stream di caratteri derivano direttamente da Reader e Writer:
 
 

Comunicazione tra thread
Le pipe di Java sono dei buffer circolari debitamente sincronizzati per consentire la comunicazione bidirezionale tra due thread, proprio come le pipe di UNIX mettono in comunicazione due processi. Una pipe è vista come una coppia di stream collegati che ricordano un po' gli stream di lettura e scrittura aperti su uno stesso socket, con la differenza che quì l'utilizzo è interno ad una JVM e totalmente realizzato in memoria.
PipedInputStream e PipedOutputStream consentono lo scambio di flussi di byte, PipedReader e PipedWriter di caratteri.
 
 

Tokenizzazione
StreamTokenizer consente di realizzare con poco sforzo una classica operazione di tokenizzazione, ovvero l'applicare un'insieme di regole ortografiche ad uno stream di caretteri in ingresso al fine di tradurlo in un flusso di entità sintattiche (token, ovvero gettoni) che, solitamente, vengono dati in pasto ad un'analizzatore sintattico (parser) secondo un modello canonico di traduttore (compilatore o interprete).
I tokenizzatori più sofisticati sono costruiti come Automi a Stati Finiti e consentono un'analisi più raffinata di quella possibile con StreamTokenizer che, invece, si limita a distinguere tra punteggiature, spazi, separatori, numeri e stringhe. Ad esempio sarebbe difficile indurre StreamTokenizer a restituire una stringa rappresentante un numero negativo, includendo il meno all'inizio, così come un floating point iniziante con il punto decimale, quanto meno perché non sarebbe in grado di verificare la non correttezza di token come "--3456" o ".3245.3.4." (in altre parole non per StreamTokenizer un carattere è punteggiatura oppure parte di un token, ma non è implementabile una logica che renda un simbolo accettabile, ad esempio, una sola volta all'interno di un token).
Tuttavia la semplicità d'uso rende questo mini tokenizzatore utile in molte occasioni.
Altro
Come avrebbe detto Ferrini: "lo dice il senso stesso della parole". LineNumberReader legge uno stream tenendo il conto delle righe partendo da 0. Il terminatore di riga può essere un '\r', un '\n' o una coppia '\r\n'.
LineNumberInputStream è stata deprecata perchè identifica i caratteri con i byte (vedere il precedente paragrafo sull'I/O di caratteri)
PushbackInputStream e PushbackReader consentono di rispedire nel buffer di lettura rispettivamente array di byte e caratteri. A patto di non sforare la capienza del buffer è anche possibile mandare in dietro, ad esempio, una stranga diversa ma di pari lunghezza dell'ultima letta dallo stream.
SequenceInputStream è una classe di utilità per eseguire il merge di più stream in ingresso, ad esempio allo scopo di concatenarli su un unico file.
 
 

Conclusioni
Come si può dedurre dal numero e dalla varietà delle classi della libreria java.io, essa può essere considerata come uno dei fulcri di tutta la programmazione Java, al pari di java.lang e del package java.util del quale parleremo il prossimo mese.
 
 
 

Risorse

    JDK Documentation (JavaSoft website). Il sito ufficiale di documentazione per il JDK 1.2
    Changes and Release Notes for the JDK 1.2 Software. Cambiamenti e novità nell'ultima versione del JDK.
    1.1 Packages- java.io, java.net
    The Java Class Libraries
    The Java Class Libraries-Second Edition, Vol. 1-1.2 Supplement
    Serializzation Specification
    Serialization Enhancements
    Essential Java Classes
    Concurrent Programming in Java

MokaByte rivista web su Java

MokaByte ricerca nuovi collaboratori
Chi volesse mettersi in contatto con noi può farlo scrivendo a mokainfo@mokabyte.it