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
|