Reader
e Writer
A partire dalla versione 1.1 del JSDK sono stati inseriti
nuovi stream orientati ai caratteri: i Reader e i Writer.
Tecnicamente si tratta di stream a tutti gli effetti,
ma non esiste un legame di ereditarietà con le
classi InputStream e OutputStream: al contrario essi
sono a capo di una gerarchia di Reader e Writer simile
a quella di InputStream ed OutputStream. La gerarchia
di Reader e Writer verrà approfondita nei prossimi
paragrafi: prima verrà illustrata l'interfaccia
di programmazione delle classi Reader e Writer, simili
per molti versi a quelle di InputStream ed OutputStream
ma con alcune sottili differenze. Ecco un elenco dei
metodi di Reader:
boolean
ready()
Controlla
se il Rader è pronto per la lettura.
int
read()
Legge
un singolo carattere e lo restituisce sotto forma di
intero a 16 bit (i 16 bit di ordine superiore vengono
scartati). In caso sia stata raggiunta la fine dello
stream, viene restituito il valore -1.
int
read(char[] cbuf)
Legge
una serie di caratteri e li restituisce nell'array passato
come parametro. Se non ci sono ulteriori caratteri da
leggere, il metodo restituisce il valore -1.
long
skip(long n)
Salta
i successive n caratteri. Il metodo restituisce il numero
di caratteri effettivamente saltati.
abstract
void close()
Chiude
il Reader e rilascia le risorse ad esso associate.
L'interfaccia
di programmazione di Writer, per quanto simile a quella
di OutputStream, presenta maggiori differenze, visto
che dispone di metodi che permettono di scrivere caratteri
o di effettuare una write() utilizzando come parametro
una String:
Writer
append(char c)
Aggiunge il carattere c in coda al Writer. Il metodo
restituisce il Writer stesso.
void
write(char[] cbuf)
Scrive
un array di char nel Writer.
void
write(int c)
Scrive
un singolo carattere sotto forma di intero a 16 bit
(i 16 bit di ordine superiore vongono ignorati)
void
write(String str)
Scrive
il contenuto della String passata come parametro.
abstract
void flush()
Forza
lo svuotamento del Writer.
abstract
void close()
Effettua
una flush(), quindi chiude il Writer e rilascia le risorse
ad esso associate.
I
metodi di entrambe le classi possono generare una IOException
qualora l'operazione incontri problemi di comunicazione.
Gerarchia
di Reader e Writer e concatenazione tra Stream
Il package java.io contiene numerose sottoclassi concreti
delle classe astratte Reader e Writer: le figure 1 e
2 mostrano una rassegna delle più importanti:
Figura 1 - Una rassegna delle principali sottoclassi
di Reader.....
Figura 2 - ....ed una di quelle di Writer
Alcune
di queste classi sono fatte per essere utilizzate così
come sono, altre sono studiate per essere usati in congiunzione
con altri Reader o Writer, ai quali possono essere collegati
in sequenza mediante concatenazione. E' questo ad esempio
il caso di BufferedReader, che tipicamente viene usato
per bufferizzare un altro tipo di Reader, come FileReader.
Per concatenare due Stream è sufficiente specificare
nel costruttore del Reader contenitore il reference
allo stream contenuto:
BufferedReader
in = new BufferedReader(new FileReader("readme.txt"));
Figura 3 - Un esempio di concatenazione tra stream
Nota: Anche gli InputStream e gli OutputStream offrono
la possibilità di concatenare vari Stream tra
loro. Il processo segue le stesse identiche regole,
e per questo motivo non è stato presentato prima,
data la scelta di concentrare la presente trattazione
sugli stream orientati ai caratteri. Una semplice consultazione
della Javadoc permetterà a qualunque programmatore
che abbia compreso il meccanismo di operare senza alcun
problema concatenazioni tra Stream.
Nei
prossimi paragrafi verranno illustrate le principali
sottoclassi di Reader e Writer.
InputStreamReader
ed OutputStreamWriter
Le classi InputStreamReader e OutputStreamWriter servono
a concatenare gli stream orientati ai caratteri con
quelli orientati ai byte. Non bisogna infatti dimenticare
che, per quanto Java ricorra al sistema di codifica
dei caratteri Unicode a 16 bit, le periferiche di Input/Output
principali, come il disco, la rete e la stampante, operano
ancora su byte. La conversione in byte di caratteri
Unicode segue una serie di regole non banali, incorporate
in questi stream, che operano una vera e propria traduzione
dal mondo dei byte a quello dei char e viceversa. I
metodi di questa coppia di stream sono esattamente gli
stessi di Reader e Writer; l'unica particolarità
da segnalare sono i costruttori, che richiedono il passaggio
di un apposito stream verso il quale viene operata la
concatenazione:
InputStreamReader(InputStream
in)
OutputStreamWriter(OutputStream out)
FileReader
e FileWriter
Gli stream a caratteri più utilizzati in assoluto
sono quelli che permettono di leggere e scrivere su
File. Tali stream non hanno metodi diversi da quelli
presenti sugli stream di base. I costruttori invece
accettano come argomento un file o una stringa che descrive
il percorso verso un file:
FileReader(File
file)
FileReader(String fileName)
FileWriter(File
file)
FileWriter(String fileName)
FileWriter
dispone di un'altra coppia di costruttori che accettano
un'ulteriore parametro booleano append, che permette
di specificare se si desidera che il file venga riaperto
e prolungato (true) o sovrascritto (false):
FileWriter(File
file, boolean append)
FileWriter(String fileName, boolean append)
BufferedReader e BufferedWriter (LineNumberReader)
Gli
stream BufferedReader e BufferedWriter dispongono al
loro interno di un buffer, ossia di una piccola memoria
cache che in molti casi sveltisce la comunicazione.
I costruttori di queste classi richiedono la specifica
di uno stream da concatenare. Molto spesso essi vengono
usati in abbinamento a FileReader e FileWriter:
BufferedReader(Reader
in)
BufferedWriter(Writer out)
Il
BufferedReader dispone di un metodo readLine() che restituisce
una String corrispondente ad un'intera linea di testo.
Similmente il BufferedWriter dispone del metodo newLine()
che stampa un carattere di invio (che per quanto sembri
banale, è diverso a seconda della piattaforma
sottostante). La classe BufferedWriter dispone di un'ulteriore
sottoclasse, LineNumberReader, che permette di conoscere
il numero di linee lette grazie al metodo getLineNumber().
Grazie
a FileReader e BufferedReader è possibile scrivere
una versione migliorata del programma di echo presentata
nel precedente articolo:
import
java.io.*;
public
class FileEcho2 {
public static void main(String argv[]) throws Exception
{
BufferedReader in = new BufferedReader(new FileReader(argv[0]));
while(true) {
String s = in.readLine();
if(s==null)
break;
else
System.out.println(s);
}
in.close();
}
}
Si
noti l'uso concatenato di BufferedReader e FileReader,
e l'uso del metodo readLine() che rende molto più
pratica la lettura e la stampa dei dati. Infine si noti
l'uso del metodo close() alla fine del programma, omesso
per semplicità dagli esempi precedenti, che dovrebbe
sempre concludere qualunque operazione su stream.
StringReader
e StringWriter
Lo stream StringReader permette di leggere una stringa
attraverso i metodi caratteristici di un Reader:
StringReader(String
s)
In
modo simile, StringWriter permette di scrivere su uno
StringBuffer con i metodi caratteristici della classe
Writer. La classe StringWriter dispone di due metodi
caratteristici:
StringBuffer
getBuffer()
String toString()
Il
primo restituisce lo StringBuffer che viene utilizzato
come contenitore di dati; il secondo fornisce una rappresentazione
in formato String dello StringBuffer stesso.
PrintWriter
Il PrintWriter è uno stream ottimizzato per scrivere
dati eterogenei su un dispositivo. Esso dispone infatti
di una serie di metodi print() che permettono di scrivere
su stream qualunque tipo di dato primitivo, oltre alle
String. I costruttori permettono di creare PrintWriter
che scrivono su un File, descritto attraverso un apposito
oggetto File o da un percorso in forma di String, oppure
su un OutputStream o da un Writer:
PrintWriter(File
file)
PrintWriter(String fileName)
PrintWriter(OutputStream out)
PrintWriter(Writer out)
Una
serie di metodi print() permettono di stampare qualsiasi
dato di tipo primitivo, o in alternativa un vettore
di char, un oggetto String o un generico Object, nel
qual caso viene invocato automaticamente il metodo toString();
void
print(boolean b)
void print(char c)
void print(double d)
void print(float f)
void print(int i)
void print(long l)
void print(char[] s)
void print(String s)
void print(Object obj)
In
secondo luogo viene fornita una serie di metodi println()
che stampano sullo stream l'argomento del metodo, seguito
da un carattere "a capo":
void
println()
void println(boolean x)
void println(char x)
void println(double x)
void println(float x)
void println(int x)
void println(long x)
void println(char[] x)
void println(String x)
void println(Object x)
A
partire dalla release 1.5, la classe PrintWriter dispone
anche di un metodo printf() simile a quello presente
nel C, che permette di stampare un output formattato:
PrintWriter
printf(String format, Object... args)
Si
noti che il secondo parametro utilizza la nuova sintassi
"..." per parametri multipli (cfr [1]). Il
metodo richiede come parametro una String che contiene
sia il testo da stampare che una serie di direttive,
precedute dal carattere %, che servono a specificare
il formato in cui devono essere stampati gli oggetti
passati nei parametri successivi. Si veda un esempio
informale:
out.printf("Il
conto corrente numero %d del Signor %s ha un saldo di
%f Euro", cc.getNumber(), cc.getOwner(),
cc.getValue());
In
questo caso, al momento della stampa, le direttive %d,
%s e %f verranno sostituite dai valori dei parametri
successivi, sotto forma di numero decimale, stringa
e valore floating point decimale con la virgola:
Il
conto corrente numero 567042 del Signor Mario Rossi
ha un saldo di 2542000,00 Euro
Nelle
righe seguenti vengono elencate le direttive permesse
dal comando e il relative effetto;
- '%b'
o '%B': l'argomento corrispondente deve essere di
tipo boolean o Boolean, il valore stampato pertanto
sarà true o false, a seconda del valore dell'oggetto.
- '%h'
o '%H': l'argomento può essere un generico
Object, che viene riportato sotto forma di numero
esadecimale chiamando il metodo Integer.toHexString(arg.hashCode()).
- '%s'
o '%S': l'argomento corrispondente può essere
un oggetto di qualunque tipo: se implementa l'interfaccia
java.util.Formattable, allora viene invocato il metodo
formatTo(), altrimenti il risultato viene ottenuto
dal classico metodo toString().
- '%c'
o '%C': l'argomento deve essere un char o un Character.
Il risultato è la stampa del carattere stesso.
- '%d':
il valore deve essere di tipo int, long, Integer o
Long, che viene riportato sotto forma di numero decimale.
- '%o'
il valore deve essere di tipo int, long, Integer o
Long, che viene riportato come numero in base otto.
- '%x'
o '%X': il valore deve essere di tipo int, long, Integer
o Long, che viene formattato come intero esadecimale.
- '%e'
o '%E': l'argomento deve essere di tipo float, double,
Float o Double. Il risultato è un numero decimale
formattato secondo la notazione scientifica computerizzata
(ad esempio "1.020030e+06")
- '%f':
l'argomento deve essere di tipo float, double, Float
o Double, che viene trascritto in forma decimale con
la virgola (ad esempio "19342.3234").
- '%g'
o '%G': l'argomento deve essere di tipo float, double,
Float o Double; il risultato è un numero floating
point formattato usando la notazione scientifica o
quella decimale a seconda della precisione e del valore
dopo l'arrotondamento.
- '%a'
o '%A': l'argomento deve essere di tipo float, double,
Float o Double, il risultato viene riportato come
numero floating point esadecimale.
- '%t'
o '%T': questa direttiva deve essere seguita da un
apposito carattere di formattazione scelto tra quelli
specificati nella classe java.util.Formatter. Il parametro
deve essere di tipo java.util.Date, e il risultato
dipende dal carattere specificato assieme alla direttiva
(ad esempio: minuti, secondi, giorno della settimana,
mese solare, anno e così via).
- '%%':
questa direttiva non richiede un argomento; il suo
scopo è quello di stampare il carattere % stesso.
- '%n':
anche in questo caso non è richiesto un argomento;
al suo posto viene stampato un carattere di "a
capo"
Serializzazione
La serializzazione è un processo che permette
di inviare attraverso uno stream un oggetto con tutto
il suo contenuto informativo, e di ripristinarlo successivamente.
Per trasmettere o ricevere oggetti si usano gli appositi
stream ObjectInputStream e ObjectOutputStream, che dispongono
rispettivamente di un metodo readObject() e writeObject(Object
o).
Figura 4 - Una rappresentazione pittorica del
concetto di serializzazione
Esistono
due sistemi per rendere serializzabile un oggetto. Il
più semplice è quello di dichiarare una
classe che implementi l'interfaccia Serializable, un'interfaccia
priva di metodi che funziona solo da flag per il compilatore:
la serializzazione vera e propria viene effettuata dallo
stream, che trasforma il valore di ciascuno dei campi
in formato binario e lo scrive sul canale di output.
L'unica condizione necessaria a rendere serializzabile
una classe è che i suoi attributi siano di tipo
primitivo o oggetti a loro volta serializzabili. Molte
classi presenti nel JSDK sono serializzabili, e questo
rende abbastanza semplice ricorrere a questo sistema.
L'unico difetto di questo meccanismo di serializzazione
è che il formato binario in cui i dati vengono
rappresentati può risultare incompatibile tra
una versione e l'altra del JSDK, o addirittura della
classe stessa, qualora vengano aggiunti o tolti attributi.
Nota:
la serializzazione opera unicamente sugli attributi
di un oggetto, non sui metodi. Questo fatto ha molta
importanza nella trasmissione di oggetti attraverso
la rete: l'host ricevente deve infatti possedere una
copia della classe per poter operare correttamente
la deserializzazione.
Alcuni
oggetti non possono essere serializzati in questo modo.
Ad esempio, una connessione ad un data base non può
essere resa permanente solamente salvando il suo stato
corrente: per ripristinarla è necessario effettuare
nuovamente la connessione. In caso come questo, in cui
l'interfaccia Serializable, non è applicabile,
si può ricorrere all'interfaccia Externalizable,
caratterizzata dai seguenti metodi:
void
writeExternal(ObjectOutput out)
Questo
metodo deve contenere le direttive per trasferire il
contenuto informativo dell'oggetto nell'apposito oggetto
ObjectOutput passato come parametro, che prevede una
serie di metodi per scrivere su stream valori primitivi
ed oggetti. Il metodo può generare una IOException
qualora si verifichi qualche problema durante la scrittura.
void
readExternal(ObjectInput in)
Questo
metodo deve contenere una serie di direttive speculari
a quelle presenti nel metodo precedente, che permettano
di ripristinare lo stato preesistente dell'oggetto serializzato
a partire dall'oggetto ObjectInput presente come parametro
della chiamata, che a sua volta dispone di una serie
di metodi per leggere valori primitivi o oggetti. Il
metodo può generare sia una IOException che una
ClassNotFoundException, nel caso una delle classi da
ripristinare non venga trovata.
Esempio
di serializzazione e deserializzazione su file
Per illustrare il funzionamento pratico della serializzazione,
è utile introdurre un esempio funzionante. Il
primo file di questo esempio è una semplice classe
dati che implementa l'interfaccia Serializable. I tre
campi che costituiscono il contenuto informativo di
questa classe sono un int, un float e una String, tutti
attributi a loro volta serializzabili:
import
java.io.*;
public
class SerializableObject implements Serializable {
private int i;
private float f;
private String s;
public SerializableObject(int i,float f,String s) {
this.i = i;
this.f = f;
this.s = s;
}
public int getI() {
return i;
}
public float getF() {
return f;
}
public String getS() {
return s;
}
}
La
seconda classe dell'esempio crea un oggetto SerializableObject,
ne stampa a schermo il contenuto, apre un ObjectOutputStream
concatenato con un FileOutputStream e serializza sul
file object.ser l'oggetto:
import
java.io.*;
public
class Serializer {
public static void main(String argv[]) throws Exception
{
SerializableObject o = new SerializableObject(10,2.253E12F,"Stringa");
System.out.printf("L'oggetto da serializzare contiene
i seguenti valori: i=%d, f=%e, s=%s",o.getI(),
o.getF(),o.getS());
ObjectOutputStream out = new ObjectOutputStream(new
FileOutputStream("object.ser"));
out.writeObject(o);
out.close();
}
}
Il
file object.ser è in formato binario: se si prova
ad aprirlo con un programma tipo wordpad il risultato
è qualcosa del genere:
í
sr SerializableObject"
;Õ6Ù÷D F fI iL st Ljava/lang/String;xpT
$Q
t Stringa
Come
si vede si tratta di un formato illeggibile per un essere
umano. L'ultima classe dell'esempio è speculare
alla precedente: dapprima viene aperto un ObjectoInputStream
concatenato ad un FileInputStream che legge il file
object.ser, quindi viene letto dallo stream un oggetto
di tipo SerializableObject (si noti la necessità
di ricorrere al casting), infine si stampa a schermo
il contenuto dell'oggetto appena deserializzato:
import
java.io.*;
public
class Deserializer {
public static void main(String argv[]) throws Exception
{
ObjectInputStream in = new ObjectInputStream(new FileInputStream("object.ser"));
SerializableObject o = (SerializableObject)in.readObject();
System.out.printf("L'oggetto serializzato contiene
i seguenti valori: i=%d, f=%e, s=%s",o.getI(),o.getF(),o.getS());
in.close();
}
}
L'output
del programma dimostra che l'oggetto è stato
serializzato e deserializzato correttamente:
L'oggetto
serializzato contiene i seguenti valori: i=10, f=2.253000e+12,
s=Stringa
Come
si può vedere l'uso della serializzazione non
presenta particolari complicazioni, e per questa ragione
può essere utilizzata ogni qual volta possa tornare
utile. L'unica accortezza è quella di leggere
gli oggetti serializzati nello stesso ordine in cui
sono stati scritti, pena una ClassCastException e la
conseguente terminazione del programma.
Conclusioni
Questo è stata illustrato l'Input/Output attraverso
gli stream orientati ai caratteri. Dopo una prima introduzione
delle classi Reader e Writer, sono stati illustrati
in profondità le sottoclassi di questi stream.
Quindi è stata illustrata la serializzazione,
un processo attraverso il quale è possibile inviare
in uno stream il contenuto di un intero oggetto. Il
mese prossimo verrà introdotta la programmazione
multi-threading.
Bibliografia
[1] Andrea Gini, "Java2 Standard Edition 1.5 beta
1", Mokabyte 3/2004
http://www.mokabyte.it/2004/03/jdk1_5.htm
Esempi
Scarica
qui gli esempi
|