IL PACKAGE JAVA.IO
Il package java.io fornisce le classi necessarie per effettuare input
e output su file/rete/console/aree di memoria RAM eccetera.
Il package può essere classificato secondo vari punti di vista:
• modalità di accesso alle risorse:
o sequenziale: si può scrivere o leggere come su un
nastro, non vi è accesso diretto;
o random: si può saltare da un punto all’altro della risorsa
in modo completamente libero.
• codifica dell’accesso:
o testuale: caratteri UNICODE
o binaria
• tipo di accesso:
o lettura
o scrittura
• operazioni effettuate durante l’accesso
o pura lettura e scrittura
o trasformazione dati
Ci concentreremo sull’accesso sequenziale, poiché è l’unico che
può essere effettuato indipendentemente dalla natura della risorsa
a cui si accede (l’accesso random è di norma consentito soltanto
su File tramite la classe RandomAccessFile).
Aimei - PACKAGE DI I/O IN JAVA
1
ACCESSO SEQUENZIALE
La modalità di accesso sarà sempre del tipo:
Lettura
Scrittura
Apri lo stream
Mentre c’e’ nuova
informazione
Leggi e manipola dati
Chiudi lo stream
Apri lo strema
Mentre c’e’ nuova
informazione
Scrivi informazione
Chiudi lo stream
Abbiamo 4 classi base astratte:
• InputStream e OutputStream per leggere/scrivere stream di
byte (come i file binari del C). (Un byte è soli 8 bit, non è un
carattere UNICODE)
• Reader e Writer per leggere/scrivere stream di caratteri
UNICODE
Esempio: se si legge un file di testo come sequenza di byte, Java
non riesce a interpretare i byte letti come caratteri.
In seguito, tratteremo separatamente stream di byte e di caratteri.
Aimei - PACKAGE DI I/O IN JAVA
2
SORGENTI E FILTRI
Due tipi di classi:
• sorgenti/destinazioni: permettono l’accesso a risorse fisiche,
quali ad esempio i file o un buffer di memoria (un array di byte,
una stringa). Forniscono poche operazioni di base, ma sanno
come accedere alla specifica risorsa;
• filtri: dato uno stream qualunque, aggiungono funzionalità.
Alcune consentono di leggere gruppi di informazioni, altre di
interpretarle, o di comprimerle, o di crittografarle.
In questo modo si riduce il numero complessivo di classi che
occorre scrivere: ad esempio, si scrive un filtro in grado di
crittografare i dati, e questo può funzionare tanto su file, quanto su
socket (comunicazione remota) quanto su porte seriali, eccetera.
Come si ottiene una funzionalità complessa? Per composizione
e incapsulamento.
• Si crea una sorgente;
• Si crea un filtro per ottenere la funzione desiderata intorno alla
sorgente, passandola come parametro al costruttore del filtro
• Questo filtro può essere dato in pasto ad un altro filtro, e così
via, fino ad ottenere la funzionalità desiderata (analogo ad una
Matrioska).
BufferedWriter
CryptoWriter
CompressWriter
FileWriter
Aimei - PACKAGE DI I/O IN JAVA
3
Esempio: (verrà dettagliato meglio in seguito)
import java.io.*;
…
FileOutputStream f = null;
f = new FileOutputStream("Prova.dat");
//creiamo 1 stream di output associato a un file
// tramite l'apposita classe "sorgente"
DataOutputStream os = new DataOutputStream(f);
// lo incapsuliamo in una classe di "filtraggio"
// che da' le funzionalità necessarie per
// scrivere i tipi primitivi di Java
os.writeFloat(f1);
os.writeBoolean(b1);
os.writeDouble(d1);
os.writeChar(c1);
GESTIONE ECCEZIONI
Quasi tutti i metodi delle classi di I/O lanciano una IOException o
una sua sottoclasse. Pertanto, le operazioni di input output devono
essere scritte fra try/catch o try/finally, o rilanciare le eccezioni.
DataOutputStream dos = null;
try {
dos = new DataOutputStream(
new FileOutputStream("Prova.dat"));
os.writeFloat(f1);
} catch(IOException e) {
e.printStackTrace();
} finally {
try {
if(dos != null) dos.close()
} catch(IOException e) {}
}
STREAM DI BYTE
Aimei - PACKAGE DI I/O IN JAVA
4
Due classi astratte per leggere e scrivere byte, InputStream e
OutputStream.
Una gerarchia di classi con sorgenti (in grigio) e filtri (in bianco).
Aimei - PACKAGE DI I/O IN JAVA
5
Metodi presenti in InputStream:
public int read()
Legge un byte (ma lo ritorna in un intero,
attenzione). Varianti per leggere un array.
public long
Salta un certo numero di byte, e ritorna
skip(long)
quanti byte ha saltato
public void mark() Marca una posizione per poterci ritornare
(se markSupported() ritorna true)
public void reset() Ritorna all’ultimo mark
Public void close() Chiude lo stream e librera le risorse
Metodi presenti in OutputStream:
public void write(byte b) Scrive un byte. Varianti per scrivere
array di byte
public void flush()
Se lo stream ritarda la scrittura, fa in
modo che le operazioni in sospeso
vengano eseguite.
Public void close()
Chiude lo stream e librera le risorse
Classi sorgente per l’input:
•
ByteArrayInputStream ne implementa i metodi nel caso
particolare in cui l’input è un buffer (array) di byte, passato
all’atto della costruzione del ByteArrayInputStream;
•
ne implementa i metodi nel caso
particolare in cui l’input è un file, il cui nome è passato al
costruttore di FileInputStream; in alternativa si può passare
al costruttore un oggetto File (o anche un FileDescriptor).
FileInputStream
FileInputStream f = null;
f = new FileInputStream("Prova.dat");
Aimei - PACKAGE DI I/O IN JAVA
6
Classi filtro per l’input
Caratteristica comune: accettano nel costruttore un parametro di
tipo InputStream. Il filtro può così agire su un altro filtro, e così
via.
Esempi:
• FilterInputStream modifica il metodo read() (ed
eventualmente gli altri) in accordo al criterio di filtraggio
richiesto (per default, il filtro è trasparente e non filtra niente).
Viene usata per semplificare la codifica delle classi filtro
concrete, come BufferedInputStream e DataInputStream.
• BufferedInputStream modifica il metodo read() in modo
tale da avere un input bufferizzato tramite un buffer che
aggiunge egli stesso. Questo riduce il numero di chiamate al
sistema operativo e aumenta l’efficienza.
• DataInputStream definisce metodi per leggere i tipi primitivi
di Java scritti su un file binario, come da esempio per esempio
readInteger(), readFloat(). Questi dati possono essere
scritti con un DataOutputStream.
Classi sorgente nell’output
• ByteArrayOutputStream implementa questi metodi nel caso
particolare in cui l’output è un buffer (array) di byte interno,
dinamicamente espandibile, recuperabile con toByteArray()
o toString(), secondo i casi.
• FileOutputStream implementa questi metodi nel caso particolare in cui l’output è un file, il cui nome è passato al costruttore di FileOutputStream; in alternativa si può passare al
costruttore un oggetto File (o anche un FileDescriptor)
Aimei - PACKAGE DI I/O IN JAVA
7
Classi filtro nell’output:
• Un FilterOutputStream modifica il metodo write() (ed
eventualmente gli altri) in accordo al criterio di filtraggio
richiesto (per default, il filtro è trasparente). Nuovamente, è
una classe di comodo per scrivere più facilmente i filtri.
• PrintStream definisce metodi per stampare sotto forma di
String i tipi primitivi e le classi standard (Integer, etc.) di
Java (medinte un metodo toString()). System.out è una
istanza di PrintStream. La scrittura avviene nell’encoding di
default della piattaforma, ovvero, i caratteri scritti possono
non essere UNICODE, ma ad esempio ASCII.
• DataOutputStream definisce metodi per scrivere in forma
binaria i tipi primitivi e le classi standard di Java
(writeInteger(), writeFloat(), etc.)
• BufferedOutputStream modifica il metodo write() in
modo da scrivere tramite un buffer (nuovamente, per
questioni prestazionali).
Aimei - PACKAGE DI I/O IN JAVA
8
ESEMPIO 1 – I/O DA FILE BINARIO
Si vuole scrivere un array di interi di dimensione qualunque su un
file, e poi rileggerlo.
static void scriviArray(int[] arr, String path)
throws IOException {
DataOutputStream dos = null;
try {
dos = new DataOutputStream(
new BufferedOutputStream(
new FileOutputStream(path)));
dos.writeInt(arr.length);
for(int i = 0; i < arr.length; i++)
dos.writeInt(arr[i]);
} finally {
if(dos != null)
try { dos.close(); } catch(IOException e) {}
}
}
static int[] leggiArray(String path)
throws IOException {
DataInputStream dis = null;
int[] result = null;
try {
dis = new DataInputStream(
new BufferedInputStream(
new FileInputStream(path)));
int arrLength = dis.readInt();
result = new int[arrLength];
for(int i = 0; i < result.length; i++)
result[i] = dis.readInt();
} finally {
if(dis != null)
try { dis.close(); } catch(IOException e) {}
}
return result;
}
Aimei - PACKAGE DI I/O IN JAVA
9
IL CASO DI SYSTEM.IN
In Java, il dispositivo di input standard è la variabile (static)
System.in, di tipo InputStream.
Poiché InputStream fornisce solo un metodo read() che legge
singoli byte, si usa incapsulare System.in in un oggetto dotato di
maggiori funzionalità, come ad esempio un BufferedReader,
che fornisce anche un metodo readLine().
Le stringhe lette dovranno poi essere interpretate in qualche modo,
con le funzioni di parsing ed eventualmente con la String.split()
per separare gli argomenti
import java.io.*;
class EsempioIn {
public static void main(String args[]){
int a = 0, b = 0;
BufferedReader in = new BufferedReader(
new InputStreamReader(System.in));
try {
System.out.print("Primo valore: ");
a = Integer.parseInt(in.readLine());
System.out.print("Secondo valore:");
b = Integer.parseInt(in.readLine());
}
catch (IOException e) {
System.out.println("Errore in input");
}
System.out.println("La somma vale " + (a+b));
}
}
Aimei - PACKAGE DI I/O IN JAVA
10
LA GERARCHIA DEGLI STREAM DI
CARATTERI
Le classi per l’I/O da stream di caratteri sono più efficienti di
quelle per l’I/O generico a byte, e gestiscono la conversione fra la
rappresentazione UNICODE adottata da Java e quella specifica
della piattaforma in uso (tipicamente ASCII) e della lingua adottata (cosa essenziale per il supporto dell’internazionalizzazione).
Anche qui vale il principio dell’“incapsulamento” di classi
sorgente in classi di filtraggio di un Reader o di un Writer da
parte di classi più evolute.
I metodi forniti sono del tutto analoghi a quelli degli stream, salvo
che operano su char piuttosto che su byte.
Aimei - PACKAGE DI I/O IN JAVA
11
STREAM DI CARATTERI - INPUT
Rispetto alla classe-base (astratta) Reader, una classe concreta
deve implementare read() e close(), anche se può ovviamente
ridefinire altri metodi. read() restituisce un carattere UNICODE.
Classi sorgenti notevoli:
• CharArrayReader è il corrispondente di ByteArrayInputStream nel caso di Reader: prende l’input da array di
caratteri.
• StringReader è analogo al precedente, ma usa come
sorgente un oggetto String invece di un array di caratteri.
• InputStreamReader: reinterpreta un InputStream
(stream di byte) come stream di caratteri, traslando l’input da
byte a caratteri UNICODE secondo la lingua locale prescelta.
L'uso tipico di questa classe è con lo stream System.In.
• La sua sottoclasse FileReader crea tale InputStream a
partire da un nome di file o da un oggetto File o da un
FileDescriptor.
Classi di filtraggio notevoli:
• BufferedReader aggiunge il buffering a un Reader di altro
tipo, definendo anche il metodo readLine(), che legge una
riga fino al fine linea (platform-dependent)
• FilterReader, classe di supporto che può essere usata per
realizzare un filtro con poco sforzo.
Non ci sono classi di filtraggio che "interpretano" i caratteri letti
(p.e., non c'è un DataInputReader).
Aimei - PACKAGE DI I/O IN JAVA
12
STREAM DI CARATTERI - OUTPUT
Classi sorgenti notevoli:
• CharArrayOutputStream è il corrispondente di ByteArrayOutputStream nel caso di Writer.
• OutputStreamWriter adatta un OutputStream (stream di
byte) come stream di caratteri, traslando l’output da caratteri
UNICODE a byte secondo la lingua locale prescelta. L'uso
tipico è con System.Out.
• La sua sottoclasse FileWriter crea tale OutputStream a
partire da un nome di file o da un oggetto File o da un
FileDescriptor.
Classi di Filtraggio Notevoli:
• BufferedWriter aggiunge il buffering a un Writer di altro
tipo definendo anche il metodo newLine() per emettere il fine
linea (platform-dependent)
• FilterWriter è una classe base che permette di scrivere con
sforzo ridotto un filtro.
Non ci sono classi di filtraggio che "interpretano" i dati da
scrivere (p.e., non c'è un DataOutputReader).
Aimei - PACKAGE DI I/O IN JAVA
13
ESEMPIO 2 – I/O DA FILE DI TESTO
static void scriviSuFile(BufferedReader input,
String file)
throws FileNotFoundException, IOException {
BufferedWriter bw = null;
try {
bw = new BufferedWriter(
new FileWriter(file));
String line = null;
while(!(line = input.readLine()).equals("")){
bw.write(line);
bw.newLine();
}
} finally {
if(bw != null)
try {
bw.close(); // questo implica la flush
} catch(Exception e) {}
}
}
static void stampaSuOutput(PrintStream out,
String file)
throws FileNotFoundException, IOException {
BufferedReader br = null;
try {
br = new BufferedReader(new FileReader(file));
String line = null;
while((line = br.readLine()) != null) {
out.println(line);
}
} finally {
if(br != null)
try {
br.close();
} catch(Exception e) {}
}
}
Aimei - PACKAGE DI I/O IN JAVA
14
SERIALIZZAZIONE DI OGGETTI
Serializzare un oggetto significa trasformarlo in uno stream di
byte, salvabile su file; analogamente, deserializzare un oggetto
significa ricostruirlo a partire dalla sua rappresentazione su file.
Una classe serializzabile deve implementare l'interfaccia
Serializable (è vuota, funge da marcatore).
• ObjectOutputStream è la sottoclasse di OutputStream
usata per serializzare un oggetto
• ObjectInputStream è la sottoclasse di InputStream usata
per deserializzare un oggetto
Queste due classi funzionano in larga misura come DataInputStream e DataOutputStream, con una differenza: aggiungono
la capacità di scrivere/leggere oggetti non primitivi su/da uno
stram di byte.
Un oggetto viene
• serializzato invocando il metodo writeObject() di
ObjectOutputStream;
• deserializzato invocando il metodo readObject() di
ObjectInputStream.
Questi metodi scrivono / leggono tutti i dati, inclusi i campi privati
e quelli ereditati dalla superclasse.
I campi dati che sono riferimenti a oggetti sono serializzati invocando ricorsivamente writeObject() sull'oggetto referenziato:
serializzare un oggetto può quindi comportare la serializzazione di
un intero grafo di oggetti. L'opposto accade quando si deserializza
un oggetto.
NB: se uno stesso oggetto è referenziato più volte nel grafo, viene
serializzato una sola volta, onde evitare che writeObject() cada
in una ricorsione infinita.
Aimei - PACKAGE DI I/O IN JAVA
15
CAMPI TRANSIENTI
A volte, non tutti i campi dati di un oggetto devono essere
serializzati.
Alcuni dati, infatti, possono non essere significativi (ad esempio,
la posizione di un cursore grafico) in quanto devono essere ricalcolati o resettati all'atto del ricaricamento dell'oggetto (ad esempio, il cursore può dover essere riposizionato al centro dell'area).
È possibile distinguere i campi da non serializzare etichettandoli transient.
PERSONALIZZAZIONE DELLA SERIALIZZAZIONE
A volte, può essere necessario personalizzare il modo in cui una
classe viene serializzata e deserializzata.
Per fare questo, occorre definire nella classe i due metodi
(privati!) writeObject() e readObject().
GESTIONE DELE VERSIONI
La versione serializzata di un oggetto contiene un numero di
versione, che è importante per verificare se la classe è in grado di
deserializzare l'oggetto.
Infatti, una nuova versione di una classe potrebbe non essere in
grado di deserializzare un oggetto di una versione precedente della
stessa classe.
Aimei - PACKAGE DI I/O IN JAVA
16