La gestione di file e flussi in Java: note introduttive

Università degli Studi di Palermo
Facoltà di Ingegneria
La gestione di file e flussi in Java:
note introduttive
Edoardo Ardizzone & Riccardo Rizzo
Appunti per il corso di
Fondamenti di Informatica
A.A. 2005- 2006
Corso di Laurea in Ingegneria Informatica
La gestione dei flussi e dei file in Java – Edoardo Ardizzone & Riccardo Rizzo
Introduzione alla gestione delle eccezioni
Se durante l’esecuzione di un metodo si verifica una situazione di errore capace di alterare il
normale flusso di esecuzione del programma, è buona norma tentare di gestirla, trasferendo il
controllo dal punto in cui l’errore viene individuato ad un appropriato gestore, ossia un gruppo di
istruzioni capaci di ripristinare una situazione di funzionamento corretto o almeno di fornire
all’utente informazioni sufficienti a capire cosa sta avvenendo. Non necessariamente questo gestore
coincide con il metodo chiamante. Per esempio, nelle operazioni di accesso ad un file possono
verificarsi delle condizioni che impediscono la corretta esecuzione delle operazioni desiderate: il
file specificato non esiste, il disco rigido è pieno e non permette di scrivere, il file sul quale si vuole
scrivere è protetto in scrittura, etc. Non è detto che il metodo che ha richiesto l’accesso al file
sappia come fronteggiare tali situazioni.
Una soluzione classica al problema della gestione degli errori prevede la restituzione di
informazioni sulla corretta (o meno) conclusione delle operazioni previste da un metodo attraverso
un valore di ritorno convenzionale (per esempio un intero positivo o negativo, per indicare il
successo o il fallimento). Tale approccio però può dare diversi problemi: il modulo chiamante può
non essere attrezzato per controllare il valore di ritorno, oppure può non essere competente, cioè in
grado di adottare una soluzione efficace. Per esempio, se si verifica un errore dovuto alla
conclusione inattesa dei dati letti da un file durante l’esecuzione di un metodo di libreria come
next() della classe Scanner, molto probabilmente il metodo che ha invocato next() non ha
informazioni sufficienti per il ripristino di una condizione di funzionamento corretta.
Nella documentazione Java, una eccezione (o evento eccezionale) è definita come un evento
che, verificandosi durante l’esecuzione di un programma, disarticola il normale flusso di esecuzione
delle istruzioni del programma [1].
Java fornisce un meccanismo efficiente e flessibile per la gestione delle eccezioni: quando in
un metodo si verifica un errore, il metodo crea un oggetto e lo passa al sistema Java. Tale oggetto,
chiamato oggetto eccezione, contiene informazioni sull’errore, come il tipo dell’errore stesso e lo
stato del programma al suo verificarsi. La creazione dell’oggetto eccezione e il suo passaggio al
sistema Java costituiscono il lancio dell’eccezione.
Dopo che un metodo ha lanciato (o sollevato) una eccezione, il sistema Java tenta di trovare
qualcosa di utile per la gestione dell’eccezione. Questa ricerca avviene nella pila delle attivazioni
dei metodi (call stack). Un esempio di call stack è mostrato in fig. 1.
Fig. 1 – Call stack [1]
2
Fig. 2 – Ricerca di un gestore appropriato [1]
La gestione dei flussi e dei file in Java – Edoardo Ardizzone & Riccardo Rizzo
Il sistema Java cerca nella pila delle attivazioni un metodo contenente un blocco di istruzioni
capace di gestire l’eccezione, cioè il gestore della eccezione (exception handler). La ricerca inizia
nel metodo in cui si è verificato l’errore e continua nella pila delle attivazioni nell’ordine inverso a
quello delle invocazioni di metodo. Se trova un gestore appropriato, il sistema Java gli passa
l’eccezione. Un gestore è considerato appropriato se il tipo dell’eccezione sollevata è compatibile
con il tipo di eccezioni che il gestore può trattare. In questo caso, il gestore cattura l’eccezione.
Se invece il sistema Java, dopo aver effettuato una ricerca esaustiva in tutti i metodi dello
stack, non trova un exception handler appropriato (è l’esempio di fig. 2), il programma termina.
Riassumendo, se in una porzione di codice potenzialmente problematica (opportunamente
inserita, come si vedrà, all'interno di uno specifico blocco) si solleva una eccezione, il controllo
viene passato ad un altro blocco (se esiste) capace di gestirla nella maniera opportuna. Il
programmatore deve scrivere solo il codice di controllo che verifica la situazione anomala e
sollevare la corrispondente eccezione. Le eccezioni vengono infatti gestite dal sistema Java in modo
da non potere essere trascurate, attraverso una tecnica di rilancio delle eccezioni stesse, fino alla
cattura da parte di un gestore competente o alla fine del programma. Questa strategia può essere
riassunta dalla frase “lanciare presto, catturare tardi”.
Naturalmente, prima che una eccezione possa essere catturata, deve essere sollevata, o da un
metodo scritto dal programmatore, o da un metodo presente in un package scritto da altri
programmatori, o da un metodo delle Java API, o dallo stesso sistema Java. Indipendentemente da
quale sia la porzione di codice che solleva l’eccezione, il lancio avviene con la clausola throw.
E’ importante notare ancora che le eccezioni in Java sono oggetti. Ne esistono predefinite,
raggruppate per classi nelle Java API, in modo da permettere ai programmi di distinguere tra diversi
tipi di errori, ma il programmatore è comunque libero di definire nuove classi di eccezioni, per
rappresentare in modo più preciso i problemi che possono presentarsi nel nuovo codice da lui
scritto.
Tutte le classi di eccezioni discendono dalla classe Throwable del package java.lang,
secondo la struttura gerarchica parzialmente mostrata in fig. 3. Le sue sottoclassi dirette sono
Exception, che raggruppa eccezioni sollevate quando si verificano errori considerati recuperabili
da un normale programma utente, come file non trovato o indice di array fuori dai limiti, e Error
che invece fa riferimento a gravi problemi relativi al funzionamento della macchina virtuale, tali da
provocare situazioni normalmente considerate irrecuperabili dai programmi comuni. Le eccezioni
della classe Error e delle sue sottoclassi sono lanciate direttamente dalla JVM e provocano la fine
del programma. Da ora in poi ci si concentrerà pertanto solo sulle eccezioni appartenenti alla classe
Exception e alle sue sottoclassi.
3
La gestione dei flussi e dei file in Java – Edoardo Ardizzone & Riccardo Rizzo
Object
Throwable
Exception
Error
ClassNotFound
Exception
RunTime
Exception
EOF
Exception
IllegalArgument
Exception
NumberFormat
Exception
FileNotFound
Exception
Arithmetic
Exception
UnknownHost
Exception
IndexOutOfBounds
Exception
IOException
ArrayIndexOutOf
BoundsException
Fig. 3 – Gerarchia delle classi di eccezioni (parziale)
Un semplice esempio per illustrare quanto il programmatore deve fare per il lancio di una
eccezione è il seguente. Ricordando l’applicazione sulla gestione dei conti correnti bancari vista in
altra parte del corso, si supponga di voler modificare il metodo preleva della classe
ContoCorrente, riportato di seguito per comodità:
public class ContoCorrente
{
….
public void preleva( double unaSomma )
//preleva denaro dal conto corrente se possibile, altrimenti stampa messaggio
{
if (saldo >= unaSomma) saldo = saldo - unaSomma;
4
La gestione dei flussi e dei file in Java – Edoardo Ardizzone & Riccardo Rizzo
else
%d",unaSomma,
}
….
}
System.out.printf
codice);
("Impossibile
prelevare
€
%.2f
dal
conto
corrente
Si vuole modificare il metodo preleva in modo che, se l’importo del prelievo risulta troppo
elevato, invece della semplice stampa di un messaggio di errore, venga lanciata una eccezione. Che
tipo di eccezione? Conviene prima di tutto cercarne una appropriata fra le classi di eccezioni
disponibili nelle Java API. Esaminando la fig. 3, la sottoclasse IllegalArgumentException
sembra utilizzabile, dato che proprio è l’argomento passato al metodo a provocare la situazione
anomala. Supponendo quindi di voler lanciare, se necessario, una eccezione di questo tipo, il
metodo può essere riscritto nel modo seguente:
public class ContoCorrente
{
….
public void preleva( double unaSomma )
//preleva denaro dal conto corrente se possibile, altrimenti lancia eccezione
{
if (saldo < unaSomma)
{
IllegalArgumentException e = new IllegalArgumentException(“Importo superiore
al saldo”);
throw e;
}
saldo = saldo - unaSomma;
}
….
}
E’ ovviamente anche possibile lanciare direttamente l’oggetto restituito dall’operatore new:
if (saldo < unaSomma)
throw new IllegalArgumentException(“Importo superiore al saldo”);
Il lancio dell’eccezione conclude l’esecuzione del metodo (come se fosse stata eseguita una
istruzione return), e il controllo passa non al metodo chiamante ma al gestore dell’eccezione, se
esiste, come si vedrà tra breve.
Esistono due categorie di eccezioni in Java: le eccezioni controllate (checked) e le eccezioni
non controllate (unchecked). Quando in un programma è presente un metodo che può lanciare una
eccezione controllata, il compilatore verifica che sia presente nel programma un gestore
dell’eccezione, oppure che sia comunque evidente la volontà del programmatore di reagire
all’eventuale sollevazione dell’eccezione, in modo che l’eccezione non possa essere ignorata. Ad
esempio, le sottoclassi di IOException sono relative ad eccezioni controllate. Se il gestore non esiste,
o se la volontà del programmatore non è evidente, il compilatore segnala un errore. Viceversa, il
compilatore non effettua alcuna verifica in relazione alle eccezioni non controllate. Sono non
controllate tutte le eccezioni appartenenti a sottoclassi di RunTimeExceptions o di Error. La
motivazione di questa differenza di trattamento, peraltro molto discussa nella comunità Java, è la
seguente. Una eccezione controllata descrive un problema che si ritiene possa verificarsi
indipendentemente dalla capacità o dalla attenzione del programmatore, come per esempio la fine
dei dati di un file dovuta ad un errore del disco o alla interruzione di una connessione di rete. In tal
caso, è obbligatorio per il programmatore specificare come si deve intervenire per ovviare ad una
tale condizione di errore. Viceversa, le eccezioni non controllate sono relative ad errori del
programmatore: una NumberFormatException o una ArithmeticException derivano sicuramente
5
La gestione dei flussi e dei file in Java – Edoardo Ardizzone & Riccardo Rizzo
da errori del codice che il programmatore dovrebbe evitare senza ricorrere alla installazione di un
gestore di eccezioni. Riassumendo, è obbligatorio per il programmatore gestire le eccezioni
controllate, relative a situazioni che sfuggono alla sua capacità di controllo o di trattamento, mentre
può essere da lui trascurata la gestione delle eccezioni non controllate, relative a errori di
programmazione. Questa scelta rende tra l’altro più agevole il compito dei programmatori inesperti,
anche in relazione all’elevata probabilità di verificarsi tipica degli errori di run time.
Come si è visto, rientrano tra le eccezioni controllate tutte quelle (sottoclasse IOExceptions e
discendenti) normalmente possibili nella gestione di dati in ingresso o in uscita, tipicamente quando
il programma interagisce con file e flussi, cioè con entità esterne al programma stesso, quindi
passibili di errori non imputabili al programmatore. Per esempio, la classe Scanner può essere
utilizzata per leggere dati da un file, nel modo seguente:
String fname = “…”;
FileReader r = new FileReader(fname);
Scanner in = new Scanner(r);
Una stringa contenente il nome del file da leggere viene passata al costruttore di FileReader,
in modo da istanziare un oggetto, r, che è possibile utilizzare per costruire un oggetto Scanner dal
quale leggere (come si è fatto finora per il dispositivo standard di ingresso), utilizzando i metodi
messi a disposizione dalla classe Scanner (se ne veda la descrizione nella documentazione delle
Java API).
Anche in un caso semplice come questo possono svilupparsi situazioni anomale. Il costruttore
di FileReader, per esempio, può lanciare una eccezione (controllata) FileNotFoundException.
Quindi bisogna o usare i blocchi try – catch per gestire direttamente la situazione (nel modo che
verrà illustrato tra breve) o rendere noto al compilatore, più semplicemente ma anche più
ragionevolmente (dato che molto probabilmente un metodo che legge dati di input non sa come
comportarsi se il file non esiste) che si è consapevoli del problema e che si vuole soltanto che il
metodo termini la sua esecuzione nel caso in cui l’eccezione sia sollevata, rilanciando l’eccezione.
Per fare questo, basta aggiungere alla prima riga della dichiarazione di metodo lo specificatore
throws, seguito dal tipo di eccezione che si vuole rilanciare, come in:
public class ….
{
public void myRead (String fname) throws FileNotFoundException
{
FileReader r = new FileReader(fname);
Scanner in = new Scanner(r);
….
}
}
Si noti che il tipo di eccezione sollevabile diviene in tal modo parte integrante dell’interfaccia
pubblica del metodo myRead. Ciò significa che qualunque cliente della classe che contiene il
metodo myRead sa di potere trovarsi a gestire una eccezione di questo tipo, allo stesso modo del
programmatore che usa la classe FileReader delle Java API (e infatti la stessa clausola throws,
seguita dallo stesso tipo di eccezione, completa la prima riga della descrizione del costruttore di
FileReader nella documentazione di tale classe). Questo meccanismo consente all’eccezione di
raggiungere, purché esista, un metodo capace di intraprendere una efficace azione di recupero,
evitando questo compito ai metodi di livello più basso.
Se il metodo può lanciare più eccezioni controllate di tipi diversi, basta indicarli separati da
virgole, per esempio:
6
La gestione dei flussi e dei file in Java – Edoardo Ardizzone & Riccardo Rizzo
public void myRead (String fname) throws FileNotFoundException,EOFException
Se una eccezione lanciata non ha un gestore rintracciabile nella pila delle attivazioni, come si
è detto, il programma termina con un generico messaggio di errore. Per evitare ciò, un programma
scritto professionalmente deve prevedere la presenza di gestori per tutte le eccezioni possibili, in
modo da evitare brusche conclusioni, normalmente poco gradite agli utenti. Si pensi ad un browser
puntato su una pagina web che a un certo punto diventa non accessibile (perché non più disponibile,
perché la connessione di rete si è interrotta, etc.). Certamente l’utente non si aspetta la fine
dell’esecuzione del browser, si aspetta invece che il browser si comporti in modo più “amichevole”
(come fanno la maggior parte dei browser reali), cioè segnali la condizione di errore e resti in vita,
consentendo all’utente di continuare ad operare.
Come accennato in precedenza, un gestore di eccezioni si installa utilizzando gli enunciati
try – catch. Un blocco di codice che contiene le istruzioni che possono provocare il lancio di una
eccezione è preceduto dalla parola chiave try, mentre ogni blocco destinato a gestire una delle
eccezioni è preceduto dalla parola chiave catch, come nell’esempio seguente:
try
{
//gruppo di istruzioni
//capaci di generare eccezioni
}
catch (IOException e)
{
//gruppo di linee capaci
//di gestire una eccezione di tipo
//IOException sollevata
//nel blocco try precedente
}
catch (NumberFormatException e)
{
//gruppo di linee capaci
//di gestire una eccezione di tipo
//NumberFormatException sollevata
//nel blocco try precedente
}
Un esempio di operazione che può generare eccezioni è la lettura da input attraverso la
variabile System.in che definisce il dispositivo standard di input. La lettura di byte da input è
eseguita nell’esempio seguente utilizzando il metodo read (che restituisce un intero) della classe
Reader del package java.io:
import java.io.*;
public class EsempioEccezione
{
public static void main (String args[])
{
char ch;
try
{
System.out.print(“Lettura dall'input”);
ch=(char) System.in.read();
System.out.println(ch);
}
catch (IOException e)
{
System.out.println(“Errore in input”);
}
}//fine classe EsempioEccezione
7
La gestione dei flussi e dei file in Java – Edoardo Ardizzone & Riccardo Rizzo
Il metodo read (si veda la documentazione delle Java API) può sollevare eccezioni di classe
IOException. Se durante l’esecuzione del metodo read (o in qualunque altro punto del blocco try)
viene sollevata una eccezione di questo tipo, le restanti istruzioni del blocco try non vengono
eseguite, e il controllo passa al blocco catch, che è stato impostato per catturare proprio questo tipo
di eccezioni. Se non vengono sollevate eccezioni, il blocco try termina normalmente e il blocco
catch non viene eseguito. Se durante l’esecuzione del blocco try viene sollevata una eccezione di
tipo diverso da IOException, il blocco catch non è in grado di intercettarla, e l’eccezione non è
gestita.
Nell'esempio che segue, le istruzioni all'interno del blocco try possono generare eccezioni di
due tipi:
try
{
BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
System.out.println(“Quanti anni hai?”);
String inputLine = in.readLine();
int age = Integer.parseInt(inputLine);
age++;
System.out.println(“L’anno venturo avrai “ + age + “ anni“);
}
catch(IOException e )
{
System.out.println(“Errore di input/output” + e);
}
catch(NumberFormatException e )
{
System.out.println(“L’input non era un numero“);
}
Le eccezioni che possono essere sollevate sono di due tipi, in quanto il metodo readLine può
dar luogo ad una eccezione (controllata) IOException, mentre Integer.parseInt può generare
una eccezione (non controllata) NumberFormatException. Se si verifica effettivamente
un’eccezione, il resto delle istruzioni del blocco try viene saltato e il controllo passa alla clausola
catch corrispondente.
Le azioni previste dai blocchi catch dell’esempio precedente si limitano ad informare
l’utente della causa del problema. Sarebbero invece opportune delle azioni che permettano
l’effettivo ripristino del programma, per esempio consentendo all’utente di reinserire i dati, come si
vedrà più avanti. In particolare, il primo blocco catch utilizza una chiamata (implicita) al metodo
toString() della classe IOException (in realtà ereditato dalla classe Throwable), che fornisce
una descrizione in forma di stringa dell’errore che si è verificato. Oltre al metodo toString(),
sono da segnalare, per tutte le classi di eccezioni definite nelle Java API, i metodi
printStackTrace() e getMessage(), anch’essi ereditati dalla classe Throwable, che permettono
di ottenere, rispettivamente, un elenco della catena di invocazioni di metodi che ha portato al lancio
della eccezione e la stringa contenente il messaggio fornito come argomento al costruttore
dell’oggetto eccezione.
E’ da notare, inoltre, che, data la struttura gerarchica delle classi di eccezioni (si veda la fig.
3), un blocco catch cattura qualunque eccezione della classe specificata o delle sue sottoclassi. Se
per esempio nel blocco try dell’esempio precedente fossero presenti istruzioni capaci di sollevare
una eccezione EOFException, essa verrebbe catturata dal primo blocco catch.
Se all’interno di un blocco try sono presenti istruzioni che devono essere comunque eseguite,
indipendentemente dall’eventuale precedente lancio di una eccezione, esse vanno inserite
all’interno di una clausola finally, come nell’esempio seguente:
8
La gestione dei flussi e dei file in Java – Edoardo Ardizzone & Riccardo Rizzo
FileReader r = new FileReader (fname);
try
{
Scanner in = new Scanner( r );
// una o più operazioni sul file
}
finally
{
r.close();
}
Se l’esecuzione si svolge normalmente, l’istruzione di chiusura del file (metodo close())
viene regolarmente eseguita dopo la fine delle operazioni sul file. Se viceversa durante una di
queste operazioni viene lanciata una eccezione, la clausola finally viene comunque eseguita,
prima di passare l’eccezione al suo gestore, e il file viene chiuso. Se l’operazione di chiusura avesse
fatto parte del blocco try, non sarebbe stata eseguita in presenza di una eccezione, e il file sarebbe
rimasto aperto.
Qualora nessuna delle eccezioni predefinite descriva una particolare condizione di errore,
come si è detto, il programmatore può definire una propria classe di eccezioni. Riprendendo ancora
una volta l’esempio del conto corrente bancario, si supponga di volere utilizzare una nuova
eccezione InsufficientBalanceException nel caso in cui l’importo del prelievo sia superiore a
quello del saldo. Il metodo preleva potrebbe essere riscritto nel modo seguente:
public void preleva( double unaSomma )
//preleva denaro dal conto corrente se possibile, altrimenti lancia eccezione
{
if (saldo < unaSomma)
{
InsuffientBalanceException e = new InsufficientBalanceException(“Prelievo
unaSomma + “superiore al saldo di ” + saldo);
throw e;
}
saldo = saldo - unaSomma;
}
di
“
+
Naturalmente occorre definire la nuova eccezione prima di utilizzarla. La prima scelta da
compiere è se debba trattarsi di una eccezione controllata o non controllata. Aderendo alla filosofia
adottata dai progettisti Java, una eccezione progettata dal programmatore dovrebbe essere non
controllata, se riguarda una potenziale situazione di errore generata all’interno dello stesso
programma. Nell’esempio del conto corrente bancario, l’errore potrebbe infatti essere evitato
controllando l’ammontare del saldo prima dell’invocazione del metodo preleva. Una eccezione
non controllata può essere definita estendendo la classe RunTimeException o una delle sue
sottoclassi:
public class InsufficientBalanceException extends RunTimeException
{
public InsufficientBalanceException() {} // costruttore vuoto
public InsufficientBalanceException (String message)
{
super(message);
}
}
9
La gestione dei flussi e dei file in Java – Edoardo Ardizzone & Riccardo Rizzo
Si può notare la presenza di due costruttori: uno senza argomenti, l’altro che accetta una
stringa destinata a contenere un messaggio che descrive la natura dell’errore (è il messaggio
recuperabile, una volta che l’eccezione sia stata lanciata, con il metodo getMessage, che la nuova
eccezione eredita da Throwable). Questa soluzione è normalmente adottata anche per le eccezioni
predefinite. Si noti anche l’uso della parola riservata super, come prima istruzione del costruttore,
che effettua una chiamata esplicita al costruttore della superclasse, necessaria per impostare
correttamente la variabile di istanza, ereditata dalla superclasse, contenente il messaggio.
Si analizzerà adesso un esempio completo di programma con gestione di eccezioni, sia
standard sia definite dall’utente, tratto da [2]. Il programma deve leggere un file, il cui nome è
richiesto all’utente, contenente numeri reali, disposti uno per riga, a partire dalla seconda riga,
mentre il numero dei dati contenuti nel file è specificato nella prima riga. In un caso del genere,
esistono almeno due tipiche possibilità di errore: il file potrebbe non esistere, e i dati potrebbero
essere scritti in un formato errato o comunque diverso da quello previsto. Conviene suddividere
l’analisi particolareggiata di queste condizioni di errore in due parti, relative rispettivamente alla
individuazione dell’errore, e quindi al lancio della corrispondente eccezione, e alla azione di
ripristino, cioè alla cattura dell’eccezione.
Per quanto riguarda la fase di individuazione, si è già visto che un file può essere aperto
passando la stringa contenente il suo nome al costruttore di FileReader, che lancia una eccezione
FileNotFoundException se il file non esiste. Dopo la lettura dei dati, al momento della chiusura
del file il metodo close di FileReader può a sua volta lanciare una IOException se incontra un
problema. Entrambe queste eccezioni sono già definite nelle Java API e sono controllate, per cui
devono essere gestite dal programma utilizzando appropriati blocchi try – catch. Occorre invece
definire appositamente una nuova eccezione (per esempio chiamata BadDataException) per
fronteggiare la possibilità che il formato dei dati in lettura sia errato, da lanciare eventualmente
durante la lettura dei dati. Conviene che anche questa nuova eccezione sia controllata, perché il file
potrebbe contenere dati corrotti per cause che sfuggono completamente al controllo del
programmatore. In particolare, la si può definire come sottoclasse di IOException. Quindi anche
questa eccezione deve essere gestita nel programma.
Per quanto riguarda l’azione di ripristino, essa può consistere in una nuova possibilità offerta
all’utente di fornire il nome del file, nel caso si verifichi uno degli errori previsti. Tale azione di
ripristino non può che essere affidata alla parte del programma che interagisce direttamente con
l’utente, che si suppone sia il main.
Il programma consiste di tre classi: DataSetReader, che fornisce gli strumenti per leggere da
file scritti nel formato specificato e per lanciare le eventuali eccezioni, DataSetTester, che
contiene il main, e BadDataException, che definisce la nuova eccezione controllata.
// DataSetTester.java
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.Scanner;
public class DataSetTester
{
public static void main (String[] args)
{
Scanner in = new Scanner(System.in);
DataSetReader r = new DataSetReader();
boolean done = false;
while(!done)
10
La gestione dei flussi e dei file in Java – Edoardo Ardizzone & Riccardo Rizzo
{
try
{
System.out.println(“Digitare nome file: “);
String fname = in.next(); // accetta nome file da leggere
double data[] = r.readFile(fname); // legge dati da file
double sum = 0;
for (double d : data) sum = sum + d;
System.out.println(“ La somma e’ : “ + sum);
done = true;
}
catch (FileNotFoundException e)
{
System.out.println(“File non trovato.”);
}
catch (BadDataException e)
{
System.out.println(“Errore nei dati: ” + e.getMessage());
}
catch (IOException e)
{
e.printStackTrace();
}
}// fine ciclo while
} // fine main
} // fine classe DataSetTester
Si noti come l’accesso al file e la lettura dei dati vengano delegati al metodo readFile (e agli
altri metodi) della classe DataSetReader, dai quali ci aspetta che possano essere sollevate le
eccezioni:
- FileNotFoundException: dopo la stampa di un messaggio di avvertimento, viene ridata
all’utente la possibilità di digitare il nome del file;
- BadDataException: viene fornita una segnalazione dipendente in maniera più specifica
dall’errore verificatosi:
- IOException: trattandosi di un qualunque altro errore di I/O, viene stampata la traccia delle
chiamate di metodo, per consentire al programmatore un’analisi più approfondita.
L’ordine in cui sono disposte le clausole catch è importante, data la relazione gerarchica
esistente tra le classi di eccezioni. Se per esempio il blocco catch relativo a IOException
precedesse quello relativo a FileNotFoundException, dato che quest’ultima è anche una
IOException, sarebbe proprio il blocco relativo a IOException a catturare anche la
FileNotFoundException, qualora venisse lanciata, rendendo vano il tentativo di discriminare tra i
diversi tipi di errore.
//DataSetReader.java
import java.io.FileReader;
import java.io.IOException;
import java.util.Scanner;
public class DataSetReader
{
private double[] data;
public double[] readFile(String fname) throws IOException
{
FileReader r = new FileReader(fname);
try
{
Scanner in = new Scanner( r );
readData(in);
11
La gestione dei flussi e dei file in Java – Edoardo Ardizzone & Riccardo Rizzo
}
finally
{
r.close();
}
return data;
} // fine metodo readFile
private void readData(Scanner in) throws BadDataException
{
if(!in.hasNextInt()) throw new BadDataException(“Numero dati non corretto”);
int numero = in.nextInt();
data = new double[numero];
for(int i = 0; i < numero;i++)
readValue(in,i);
if(in.hasNext()) throw new BadDataException(“File non finito”);
}// fine readData
private void readValue(Scanner in, int i) throws BadDataException
{
if(!in.hasNextDouble) throw new BadDataException(“Dato double non presente”);
data[i] = in.nextDouble();
}// fine readValue
} // fine classe DataSetReader
// BadDataException.java
import java.io.IOException;
public class BadDataException extends IOException
{
public BadDataException() {}
public BadDataException(String message)
{
super(message);
}
} // fine classe BadDataException
Si noti come alla BadDataException sia associato ogni volta un messaggio diverso, a
seconda della tipologia di errore corrispondente al lancio dell’eccezione, e come ciascuno dei
metodi della classe DataSetReader si limiti a rilanciare l’eccezione appropriata, senza tentare
alcuna azione di recupero: l’eccezione viene semplicemente trasferita al metodo chiamante. Inoltre,
la presenza della clausola finally nel metodo readFile garantisce la chiusura del file anche se è
stata lanciata una eccezione. Lo specificatore throws di questo metodo non include le classi
FileNotFoundException e BadDataException, in quanto queste ultime sono sottoclassi di
IOException.
Riassumendo, e nell’ipotesi che durante la lettura dei dati (metodo readValue) si verifichi un
errore, la sequenza degli avvenimenti è la seguente:
a. Il main di DataSetTester chiama readFile di DataSetReader.
b. readFile chiama readData.
c. readData chiama readValue.
d. readValue non trova il valore aspettato e lancia un’eccezione BadDataException.
e. readValue non ha un gestore per tale eccezione e la rilancia, terminando immediatamente.
f. readData non ha un gestore per tale eccezione e la rilancia, terminando immediatamente.
g. readFile non ha un gestore per tale eccezione e la rilancia, terminando immediatamente dopo
aver eseguito la clausola finally che chiude il file.
12
La gestione dei flussi e dei file in Java – Edoardo Ardizzone & Riccardo Rizzo
h. Il main di DataSetTester ha un gestore per questa eccezione, che visualizza il messaggio
all’utente e gli dà una nuova possibilità (non vengono eseguite le istruzioni di elaborazione dei
dati).
13
La gestione dei flussi e dei file in Java – Edoardo Ardizzone & Riccardo Rizzo
La gestione di file e flussi in Java
1. File e flussi
Un file o archivio è una collezione di dati persistenti. I dati memorizzati nelle variabili di un
programma sono invece temporanei in quanto cessano di esistere al termine dell’esecuzione del
programma.
I file sono degli oggetti fisici, normalmente memorizzati in una memoria di massa, che spesso
trascendono la vita di un programma, ovvero esistono prima della sua esecuzione e continuano a
esistere dopo la fine dell’esecuzione. Sono quindi parte del sistema e per la loro gestione è
normalmente adoperata una parte importante del sistema operativo, il file system, le cui funzioni
sono rese disponibili ai programmi che devono manipolare le informazioni contenute nei file.
I programmi, in qualunque linguaggio siano scritti, devono poter creare file, e leggere,
modificare o cancellare file, eventualmente creati da altri programmi scritti in linguaggi diversi. Un
file può contenere un documento scritto con un word processor, oppure un programma sorgente in
linguaggio evoluto scritto con un editore di testi, oppure il codice oggetto generato da un
compilatore C, oppure ancora i risultati delle elaborazioni effettuate da una applicazione scritta in
Java sui dati contenuti in un database creato con un DBMS. E’ quindi evidente come tutti i
linguaggi di programmazione debbano fornire il supporto per la creazione dei file e per la
manipolazione del loro contenuto.
Nei primi linguaggi di programmazione, questo supporto era normalmente costituito da
istruzioni specifiche. La tendenza recente è invece quella di non definire delle primitive per la
gestione dei file come parte del linguaggio. Per esempio, nel caso del linguaggio C il supporto per il
trattamento dei file è disponibile attraverso la standard library. L’implementazione delle funzioni di
libreria (messe a disposizione dal sistema C) tiene conto del sistema operativo su cui il programma
viene eseguito, agevolando l’invocazione corretta delle funzioni del sistema operativo stesso.
Questo consente al programmatore di trattare i file senza doversi preoccupare dei dettagli della
gestione realizzata dal file system. Analogamente, nel caso del linguaggio Java il package java.io,
che fa parte di Java API, contiene le definizioni delle classi utilizzabili per la gestione dei file (o
meglio, dei flussi, come si vedrà tra breve).
I dati contenuti in un file sono ovviamente combinazioni di bit. Questa visione di basso livello
dei dati è particolarmente scomoda sia per il programmatore sia per gli utilizzatori di un
programma. L’uno e gli altri sono infatti abituati a rappresentare l’informazione in termini di nomi,
date, numeri, parole, etc. che assumono un significato nel contesto dell’applicazione. Per esempio,
un programma che debba elaborare delle statistiche sugli esiti di un esame degli studenti di una
classe, si baserà su una struttura logica dei dati che probabilmente prevedrà una indicazione delle
informazioni necessarie, come cognome, nome e voto, ripetuta per ciascuno degli studenti. Ci sarà
quindi un record per ogni studente, costituito per esempio dai seguenti campi:
1.
2.
3.
4.
14
Numero di matricola
Cognome
Nome
Votazione conseguita
La gestione dei flussi e dei file in Java – Edoardo Ardizzone & Riccardo Rizzo
Ogni campo contiene una informazione, che può essere espressa in forma numerica (per
esempio, la votazione può essere costituita da un numero intero) o mediante una sequenza di
caratteri (per esempio, il cognome, ma anche il numero di matricola e la stessa votazione possono
essere memorizzati in questo modo). In ogni caso, l’informazione contenuta in un campo viene
trasformata in una sequenza di bit (come è noto, in Java un carattere è memorizzato utilizzando due
byte, secondo lo standard Unicode, mentre per un numero intero, se per esempio è di tipo int,
occorrono quattro byte, il cui significato dipende dalla rappresentazione dei numeri adottata dal
sistema).
E’ lecito pertanto parlare di una gerarchia dei dati: un file è costituito da un gruppo di record
correlati, un record è costituito da un gruppo di campi correlati, un campo è costituito da un gruppo
di caratteri o da un numero, un carattere (o un numero) è memorizzato utilizzando la appropriata
sequenza di bit (fig. 4).
File
08756
07632
08541
Rossi
Bianchi
Neri
Mario
Carla
Fulvio
27
24
30
Verdi
Renata 28
Record
…
06532
Campo
Ver
00000000 01010110
(Unicode V)
Fig. 4 – La gerarchia dei dati
Esistono dunque due modalità differenti di memorizzare i dati: il formato testo e il formato
binario. Nel formato testo gli elementi relativi ai dati sono rappresentati in forma leggibile, cioè
come sequenze di caratteri. Per esempio, il numero intero 12345 può essere memorizzato in formato
testo come una sequenza di cinque caratteri: ‘1’ ’2’ ’3’ ’4’ ’5’, mentre in formato binario int può
essere memorizzato come una sequenza di 4 byte: 00000000 00000000 00110000 00111001.
Vengono invece normalmente memorizzati esclusivamente in forma binaria le immagini e i suoni.
Un altro aspetto fondamentale è il seguente. I moderni sistemi operativi mostrano ai
programmi le periferiche standard di ingresso e di uscita come file. Un programma può pertanto
realizzare un’operazione di uscita scrivendo in un particolare file che rappresenta il dispositivo
standard di uscita (Standard Output, normalmente lo schermo del terminale o del PC), e può
realizzare una operazione di ingresso leggendo da un particolare file che rappresenta il dispositivo
standard di ingresso (Standard Input, normalmente la tastiera). Risultano quindi identiche le
modalità di realizzazione delle operazioni di I/O e delle operazioni di memorizzazione permanente,
attraverso le funzionalità di basso livello messe a disposizione dal sistema operativo tramite
15
La gestione dei flussi e dei file in Java – Edoardo Ardizzone & Riccardo Rizzo
l’invocazione degli appropriati moduli delle librerie disponibili con il linguaggio di
programmazione. Per tale motivo le operazioni di lettura/scrittura su file, ma anche di trasferimento
dati da un computer ad un altro, per esempio attraverso una connessione di rete, vengono
considerate a tutti gli effetti come operazioni di ingresso/uscita eseguite su generiche entità
denominate flussi.
All’interno di un file, l’organizzazione dei record può essere di diversi tipi. La più comune è
quella sequenziale, nella quale i record sono conservati in un ordine che può essere quello
cronologico di inserimento nel file ovvero dipendere da un qualche criterio di ordinamento.
Un’altra organizzazione tipica è la sequenziale con indice, nella quale oltre al file vero e proprio
vengono conservati uno o più indici. Un indice è costituito dalla sequenza, normalmente ordinata,
dei valori che nei record del file assume un particolare campo, detto chiave, affiancati dai puntatori
ai corrispondenti record del file. In caso di inserimento di nuovi record, questa organizzazione
consente di riordinare soltanto l’indice, con guadagno di efficienza ma a discapito di una maggiore
occupazione di memoria.
L’accesso ad un file sequenziale, sia in lettura che in scrittura, avviene normalmente in modo
sequenziale, un record dopo l’altro. L’accesso diretto ad uno specifico record è invece consentito
solo in presenza della definizione di un campo chiave. Si noti che talvolta quest’ultimo può
semplicemente essere costituito dal numero d’ordine del record (attribuito automaticamente dal
sistema durante la scrittura del file) ed essere presente anche nella semplice organizzazione
sequenziale.
Ogni file termina con un marcatore di fine file, la cui natura dipende dal sistema operativo e
dal tipo di file.
Così come avviene anche per altri linguaggi di programmazione, i programmi Java eseguono
le operazioni di I/O usando il meccanismo generico dei flussi sequenziali (stream): il flusso e' un
oggetto astratto che produce (flusso di input) o consuma (flusso di output) informazioni ed è di
norma "collegato" ad un dispositivo fisico o ad un file (fig. 5). I flussi "astratti" hanno quindi tutti il
medesimo comportamento, anche se collegati a dispositivi fisici differenti.
Flusso di input
Flusso di output
Fig. 5 – Flussi [1]
Quando viene avviata l’esecuzione di un programma Java, per esempio, vengono creati
automaticamente tre oggetti flusso, System.in, System.out e System.err, che forniscono i canali
di comunicazione tra il programma e, rispettivamente, i dispositivi standard di input, di output e di
visualizzazione degli errori.
Il collegamento tra un programma e un file deve essere invece stabilito esplicitamente,
creando il corrispondente oggetto flusso. Questa operazione prende il nome di apertura del file (o
del flusso). L’operazione inversa di chiusura del file (o del flusso) interrompe il collegamento.
Da quanto detto, dovrebbe essere evidente che un flusso è una entità dalle caratteristiche più
generali, rispetto a quelle di un file.
Si noti che benché a basso livello i flussi siano comunque costituiti da byte, in Java, poiché i
caratteri sono rappresentati secondo lo standard Unicode a due byte, vengono di fatto distinti due
tipi di flussi:
• flussi di byte, utili per la lettura e scrittura di dati binari quali immagini o suoni;
16
La gestione dei flussi e dei file in Java – Edoardo Ardizzone & Riccardo Rizzo
•
flussi di caratteri, utili per la lettura o scrittura dei dati in formato testo;
Riassumendo, per scrivere o leggere i flussi occorre:
1.
2.
3.
aprire il flusso
scrivere o leggere le informazioni
chiudere il flusso.
La chiusura del flusso è molto importante: solo quando il flusso in scrittura è correttamente
chiuso si ha la certezza che i dati siano stati inviati (o scritti sul disco se si tratta di un file).
Il package java.io contiene la collezione di classi utili per la gestione dei flussi, e quindi dei
file, suddivise in due gerarchie, una per i flussi di caratteri, l’altra per i flussi di byte.
2. Flussi di caratteri
Alcune delle classi per la gestione dei flussi di caratteri presenti nelle Java API sono mostrate
in fig. 6, insieme con le loro relazioni di ereditarietà. In bianco sono mostrate le classi che,
indipendentemente dalla sorgente o dalla destinazione dei flussi, specializzano ed aumentano le
funzionalità delle rispettive superclassi. In grigio sono invece mostrate le classi che, senza
aggiungere funzionalità, specializzano le superclassi rispetto ad una specifica sorgente o
destinazione dei flussi.
Fig. 6 – Gerarchia delle classi per l’I/O di caratteri [1]
Le classi Reader e Writer, da cui derivano le due gerarchie, sono superclassi astratte che
definiscono la struttura e la parziale implementazione di flussi "astratti", non collegati a dispositivi
fisici (sono le classi FileReader e FileWriter ad essere usate per la gestione dei file su disco). Le
classi possono essere poste a confronto con quelle relative alla gestione di stream di byte e ne
costituiscono la versione capace di elaborare i caratteri.
Le classi più importanti derivate da Reader sono :
CharArrayReader
17
La gestione dei flussi e dei file in Java – Edoardo Ardizzone & Riccardo Rizzo
prende l'input da un array di caratteri; è la classe di livello più basso (corrispondente a
ByteArrayInputStream nei flussi di byte). Una classe analoga e' StringReader che è a
livello più elevato e gestisce l'input da oggetti di tipo String;
InputStreamReader
è una specializzazione di Reader in quanto ha i metodi per prendere un flusso di byte e
interpretarli come caratteri Unicode (secondo la lingua locale). Sottoclasse di questa è
FileReader che consente la lettura di file di caratteri da disco, anche se, come si vedrà,
l'input letto è un intero che successivamente è trasformato in carattere.
BufferedReader
che aggiunge un buffer a un Reader di altro tipo (tipicamente ad un FileReader). La classe
ha un metodo readLine che legge una riga fino al carattere di fine linea (il carattere di fine
linea dipende dal sistema operativo).
Mancano classi che interpretino i caratteri letti, in quanto il tipo di dato letto è sempre lo
stesso (carattere).
Da Writer discendono, tra le altre, le seguenti classi:
CharArrayWriter
che è il corrispondente di ByteArrayOutputStream, implementa cioè l'output su un array
di caratteri interno;
OutputStreamWriter
specializzazione di Writer, interpreta un flusso di byte come stream di caratteri,
trasformando i caratteri Unicode in byte secondo la lingua prescelta. La sottoclasse
FileWriter permette di scrivere su file;
BufferedWriter
e' la classe filtro che aggiunge un buffer a un Writer (per esempio a un FileWriter).
Anche in questo caso non esistono classi filtro che trasformino i dati da scrivere.
L’esempio seguente mostra un frammento di programma che legge caratteri da un file e li
scrive in un altro, utilizzando le classi FileReader e FileWriter.
// apertura dei file (flussi)
// ai costruttori vengono passate le stringhe contenenti i nomi dei file
FileReader in = new FileReader("fileinput.txt");
FileWriter out = new FileWriter("fileoutput.txt");
int i;
char c;
// lettura e scrittura dei file
while ((i = in.read()) != -1)
//in.read() legge un intero che rappresenta
{
//un carattere UNICODE
c = (char) i
//Il valore letto e' trasformato in carattere
out.write(c);
// c e' scritto nel flusso di uscita
}
// chiusura dei file
in.close();
out.close();
18
La gestione dei flussi e dei file in Java – Edoardo Ardizzone & Riccardo Rizzo
La classe FileReader ha il metodo read che legge un carattere alla volta (esso implementa
l’omonimo metodo astratto della classe Reader). Il metodo read di fatto restituisce un int che
segnala se è stato letto un carattere (un valore tra 0 e 65535) o si è raggiunta la fine del file (-1). Si
deve quindi verificare che il valore restituito sia diverso da –1 prima di assegnarlo ad un char.
Vale la pena ricordare che anche la classe Scanner introdotta con la versione 5.0 di Java può
essere utilizzata per leggere dati da un file, nel modo seguente:
FileReader r = new FileReader(“fileinput.txt”);
Scanner in = new Scanner(r);
La stringa contenente il nome del file da leggere viene passata al costruttore di FileReader,
in modo da istanziare un oggetto, r, che è possibile utilizzare per costruire un oggetto Scanner dal
quale leggere, utilizzando i metodi della classe Scanner.
Quando si deve specificare un nome di file con una stringa contenente la barra rovesciata,
occorre ricordare di scriverla due volte, per esempio:
FileReader in=new FileReader(“C:\\esercizi\\files\\input.dat”);
Se si vogliono scrivere più caratteri o numeri alla volta si deve utilizzare la classe
PrintWriter. Un’istanza di quest’ultima classe si costruisce passando al suo costruttore un oggetto
FileWriter:
FileWriter out = new FileWriter("fileoutput.txt");
PrintWriter out = new PrintWriter(fileout);
A questo punto si possono utilizzare i metodi già noti, come print e println, per scrivere
sul file numeri, stringhe o oggetti:
out.print(29.92);
out.println(new Rectangle(5,10,15,25));
out.println(“Salve Mondo!”);
Naturalmente tutto ciò che è diverso da una stringa ed è passato a print o println verrà
convertito mediante il metodo toString della classe cui appartiene.
Per leggere un file di testo si può anche utilizzare la classe BufferedReader, che con il
metodo readLine permette di leggere una riga alla volta. Dopo aver letto una riga si possono
utilizzare i metodi Integer.parseInt e Double.parseDouble per convertire le stringhe in numeri.
Se in una singola riga ci sono più stringhe, si può utilizzare la classe StringTokenizer per
suddividere l’input letto nelle sue stringhe componenti.
3. Altri esempi di scrittura e lettura di file di testo
Nel seguito si riportano altri esempi di lettura/scrittura da file di testo, con la gestione delle
relative eccezioni.
Copia di file con gestione delle eccezioni(dentro il main)
import java.io.*;
19
La gestione dei flussi e dei file in Java – Edoardo Ardizzone & Riccardo Rizzo
public class CopyFile
{
public static void main(String[] args)
{
try
{ // anche le aperture dei flussi devono stare all'interno del try
// perché possono generare eccezioni
FileReader in = new FileReader("fileinput.txt");
FileWriter out = new FileWriter("fileoutput.txt");
int i;
char c;
while ((i = in.read()) != -1)
{
c =(char) i;
out.write(c);
}
in.close();
out.close();
}
catch (IOException e)
{System.out.println("errore" + e); }
}// fine main
}// fine classe
Uso delle classi BufferedReader e PrintWriter
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
Scrivere un programma capace di eliminare da un file le
righe con carattere iniziale "#"
Per esempio dato il file di input:
riga uno
riga due
#riga tre
#riga quattro
riga cinque
L'output dovrebbe essere:
riga uno
riga due
riga cinque
import java.io.*;
public class Filtro {
public static void main(String[] args)
{
String buffer;
try
{
FileReader filein = new FileReader ("dafiltrare.txt");
FileWriter fileout = new FileWriter ("filtrato.txt");
PrintWriter out = new PrintWriter (fileout);
BufferedReader in = new BufferedReader (filein);
while ( in.ready() )
{
buffer=in.readLine();
20
La gestione dei flussi e dei file in Java – Edoardo Ardizzone & Riccardo Rizzo
buffer=buffer.trim();
if ( ! (buffer.startsWith("#") ) ) out.println(buffer);
}
filein.close();
fileout.close();
}
catch (Exception e) { System.out.println("errore" + e);
}
}
}
4. Flussi di byte
Alcune delle classi definite per la gestione dei flussi di byte sono mostrate in fig. 7, insieme
con le loro relazioni di ereditarietà.
Fig. 7 – Gerarchia delle classi per l’I/O di byte [1]
Le due gerarchie di classi derivano dalle superclassi astratte InputStream e OutputStream,
che definiscono e parzialmente implementano i flussi di byte relativi a periferiche generiche, come
mostrano le operazioni supportate.
Le principali operazioni definite da InputStream sono:
per leggere uno o più byte
• read()
• available() per sapere quanti byte sono già disponibili in input
• skip()
per saltare N byte
per ricavare la posizione corrente e reset() per ritornarci se
• mark()
markSupported()è vero.
Su OutputStream è invece possibile eseguire le seguenti operazioni:
scrivere uno o piu' byte con write();
• svuotare il buffer di scrittura con flush();
• chiudere lo stream con close().
•
Per quanto riguarda l'input binario, si notino in particolare le due classi:
• ByteArrayInputStream, che gestisce l'input proveniente da un buffer (array) di byte;
21
La gestione dei flussi e dei file in Java – Edoardo Ardizzone & Riccardo Rizzo
•
FileInputStream, che implementa i metodi necessari alla gestione di input da file. Il nome
del file è passato al costruttore direttamente tramite una stringa oppure attraverso un oggetto
di tipo File, che come si vedrà incapsula il nome del file gestendo i possibili formati per i
diversi sistemi operativi.
Tra le classi che aggiungono funzionalità, si accennerà solo alle classi ObjectInputStream,
che serve a leggere oggetti serializzati memorizzati nello strema (la serializzazione degli oggetti
verrà discussa in seguito), e DataInputStream, che definisce i metodi per leggere i dati di tipi
primitivi di Java da file binari.
Per l'output binario si segnalano le seguenti due classi:
• ByteArrayOutputStream, che implementa l'output nel caso in cui la destinazione sia un
buffer di byte interno. Il buffer è disponibile per successive elaborazioni ed è recuperabile
tramite il metodo toString();
• FileOutputStream, che implementa i metodi di output per la scrittura binaria di un file. Il
nome del file è passato al costruttore con le modalità già dette.
Tra le classi filtro si ricordano la classe ObjectOutputStream, utile per scrivere oggetti
serializzati su strema, e la classe DataOutputStream, che definisce metodi utili a scrivere in binario
i dati di tipi primitivi, in un formato indipendente dalla macchina che agevola la portabilità.
Tutti gli stream, sia di caratteri che di byte, sono automaticamente aperti al momento della
creazione, e automaticamente chiusi quando il programma termina normalmente. Tuttavia è
consigliabile provvedere esplicitamente alla chiusura (tramite l’invocazione dell’appropriato
metodo close()) quando se ne è completato l’uso, sia per liberare risorse di sistema, sia, come già
accennato, per evitare che l’eventuale fine inattesa del programma lasci il file in uno stato di
indeterminatezza (dati non completamente trasferiti in fase di scrittura, etc.).
5. Esempi di lettura e scrittura
FileOutputStream consente di scrivere byte o array di byte su file. Volendo salvare i dati di
tipi primitivi di Java è però più pratico usare DataOutputStream, incapsulando FileOutputStream
nel seguente modo:
FileOutputStream f=new FileOutputStream(“dati.dat”);
...
DataOutputStream os=new DataOutputStream(f);
Si possono quindi scrivere i dati primitivi utilizzando i metodi appropriati, come:
os.writeFloat(f1);
os.writeBoolean(b1);
os.writeDouble(d1);
os.writeChar(c1);
os.writeInt(I1);
Lo stream va quindi chiuso con os.close();.
Analogamente, per leggere da un file binario dati di tipi primitivi di Java si può usare un
FileInputStream cui aggiungere i metodi che servono alla lettura dei dati primitivi,
incapsulandolo in un DataInputStream.
FileInputStream fin=new FileInputStream(“nomedelfile.dat”);
22
La gestione dei flussi e dei file in Java – Edoardo Ardizzone & Riccardo Rizzo
DataInputStream is=new DataInputStream(fin);
float f; char c; boolean b; double d; int i;
f=is.readFloat();
b=is.readBoolean();
d=is.readDouble();
c=is.readChar();
i=is.readInt();
is.close();
Nel seguito è riportato un esempio completo di scrittura e lettura di file binari che utilizza le
classi suddette.
import java.io.*;
public class CopyFileByte
{
public static void main(String[] args)
{
int i1=0;
char c1='0';
float f1=0.0f;
double d1=0.0;
//scrittura del file binario
try
{
FileOutputStream o = new FileOutputStream("bytefile.txt");
DataOutputStream os = new DataOutputStream(o);
int i=1;
char c='c';
float f=2.3f;
double d=2.3;
os.writeInt(i);
os.writeChar(c);
os.writeFloat(f);
os.writeDouble(d);
os.close();
}
catch (IOException e) {System.out.println("errore" + e); }
//lettura del file binario
try
{
FileInputStream i = new FileInputStream("bytefile.txt");
DataInputStream is = new DataInputStream(i);
i1=is.readInt();
c1=is.readChar();
f1=is.readFloat();
d1=is.readDouble();
is.close();
}
catch (IOException e) {System.out.println("errore" + e); }
System.out.println("Integer:" + i1);
System.out.println("Char:" + c1);
System.out.println("Float:" + f1);
System.out.println("Double:" + d1);
}
23
La gestione dei flussi e dei file in Java – Edoardo Ardizzone & Riccardo Rizzo
}
Nell’esempio seguente, si procede alla codifica-decodifica di un file di testo, secondo le
modalità descritte nel commento iniziale.
\* Il programma codifica-decodifica un file di testo. Ogni carattere è sostituito con un
altro che gli succede in ordine alfabetico di tre posizioni. Si può specificare anche un
numero diverso di posizioni immettendo tale parametro nella riga di comando, insieme con
i nomi dei file di ingresso e di uscita. L’opzione –d nella riga di comando permette di
distinguere il caso di decodifica da quello della cifratura. */
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
public class Cripto
{
public static final int DEFAULT_KEY=3;
public static final int NLETTERS='z'-'a'+1;
public static void main(String[] args)
{
boolean decrypt=false;
int key=DEFAULT_KEY;
FileReader infile=null;
FileWriter outfile=null;
if(args.length<2 || args.length>4) usage();
try
{
for(int i=0; i<args.length;i++)
{
if(args[i].substring(0,1).equals("-"))
{
String option=args[i].substring(1,2);
if(option.equals("d")) decrypt=true;
else if(option.equals("k"))
{
key=Integer.parseInt(args[i].substring(2));
if(key<1 || key>=NLETTERS) usage();
}
}
else
{
if(infile==null) infile=new FileReader(args[i]);
else if(outfile==null) outfile=new FileWriter(args[i]);
}
}// fine for
}// fine try
catch(IOException e)
{
System.out.println("Errore: file non trovato!");
System.exit(0);
}
if(infile==null || outfile==null) usage();
if(decrypt) key=NLETTERS-key;
try
{
encryptFile(infile,outfile,key);
infile.close();
outfile.close();
}
catch(IOException e)
{
System.out.println("Errore nell'elaborazione del file!");
24
La gestione dei flussi e dei file in Java – Edoardo Ardizzone & Riccardo Rizzo
System.exit(0);
}
}//fine main
public static void usage()
{
System.out.println("Uso: java Cripto [-d] [-kn] infile outfile");
System.exit(1);
}
public static char encrypt(char c,int k)
{
if(c>='a' && c<='z')
return (char) ('a'+(c-'a'+k)%NLETTERS);
if(c>='A' && c<='Z')
return (char) ('A'+(c-'A'+k)%NLETTERS);
return c;
}
public static void encryptFile(FileReader in, FileWriter out, int k)
throws IOException
{
while(true)
{
int next=in.read();
if(next==-1) return;
char c=(char) next;
out.write(encrypt(c,k));
}
}
}//fine classe Cripto
Come è noto, i nomi di file e directory nei vari sistemi operativi seguono convenzioni
differenti. E’ pertanto opportuna la presenza nelle Java API della classe File che permettere di
nascondere tali differenze. La classe File fornisce metodi per creare e cancellare directory e file
stessi ma non fornisce strumenti per manipolare i contenuti dei file stessi. Un oggetto File deve
invece essere passato al costruttore di FileReader, come nel seguente frammento di programma:
JFileChooser chooser=new JFileChooser();
FileReader in=null;
if ( chooser.showOpenDialog(null) == JFileChooser.APPROVE_OPTION )
{
File selectedFile=chooser.getSelectedFile();
in = new FileReader(selectedFile);
}
La classe JFileChooser (che è parte del package Swing) consente di aprire una finestra di
dialogo per la selezione di un file. Istanziato un oggetto di questa classe, è possibile chiamare il
metodo showOpenDialog() per la scelta di un file in lettura, oppure showSaveDialog() per la
scrittura. Questi metodi restituiscono la costante JfileChooser.APPROVE_OPTION se l'utente ha
scelto il file cliccando su Open o Save oppure JfileChooser.CANCEL_OPTION se l'utente ha
annullato la selezione.
Se si e' scelto un file, il metodo getSelectedFile() restituisce un oggetto di tipo File, che
può essere passato quindi a FileReader. L’esempio seguente illustra compiutamente questi aspetti.
import javax.swing.*;
import java.io.*;
25
La gestione dei flussi e dei file in Java – Edoardo Ardizzone & Riccardo Rizzo
public class TestFile {
public static void main( String args[] ) throws IOException
{
String input;
// valore inserito dall'utente
JFileChooser win = new JFileChooser();
//finestra per la navigazione nel
//filesystem
FileWriter out = null;
//file di output al
//momento non inizializzato
input = JOptionPane.showInputDialog( "Input Stringa" );
if (win.showSaveDialog(null)==JFileChooser.APPROVE_OPTION)// se si accetta
{
//il file
//restituisce l'oggetto File collegato al file scelto
File selectedFile = win.getSelectedFile();
//e' obbligatorio catturare le eccezioni generate operando sui file
//l'oggetto File e' collegato al file dove scrivere
out = new FileWriter(selectedFile);
//quindi un oggetto PrintWrite e' collegato al file
PrintWriter outLine = new PrintWriter (out);
out
//l'oggetto PrintWriter e' usato per scrivere nel file
outLine.println(input);
outLine.println("\n");
// il file viene chiuso
out.close();
}
}
}
//fine if
System.exit( 0 );
// fine main
// fine classe
// il file e' sovrascritto ogni volta con il nuovo contenuto
6. Serializzazione di oggetti
Serializzare un oggetto significa trasformarlo in uno stream di byte da inviare ad un dispositivo di
output o salvare su un file. Deserializzare un oggetto significa invece eseguire le operazioni inverse
e quindi ricostruire l'oggetto a partire dalla sua rappresentazione in byte.
Una classe serializzabile deve implementare la interfaccia Serializable. Per la
serializzazione e deserializzazione degli oggetti sono disponibili due classi apposite,
ObjectOutputStream e ObjectInputStream, che aggiungono la capacità di leggere e scrivere
oggetti a InputStream e OutputStream, allo stesso modo in cui DataInputStream e
DataOutputStream aggiungono la capacità di leggere e scrivere dati primitivi.
Questi metodi leggono e scrivono tutti i dati, inclusi i privati e quelli ereditati dalla
superclasse. Se l'oggetto ne contiene altri l'intero grafo viene salvato e poi ricostruito. I dati che non
devono essere serializzati possono essere etichettati con la parola chiave transient. La
serializzazione di un oggetto contiene un numero di versione poiché una classe potrebbe non essere
in grado di deserializzare una versione precedente.
26
La gestione dei flussi e dei file in Java – Edoardo Ardizzone & Riccardo Rizzo
Nella lettura di una classe serializzata occorre ricordarsi di fare il cast alla classe letta dei dati
letti dal file e di catturare l'eccezione relativa a ClassNotFoundException
Esempio di classe serializzabile
import java.io.*;
public class Coordinate implements Serializable
{
private int x, y;
public Coordinate() {
x=0;
y=0;
}
public void setX(int cx) {x=cx;}
public void setY(int cy) {y=cy;}
public int getX() {return x;}
public int getY() {return y;}
}
Esempio di scrittura e di lettura della classe serializzata
import java.io.*;
public class Seri
{
public static void main(String args[]) {
//inizializza le coordinate a 0
Coordinate punto=new Coordinate();
punto.setX(2);
punto.setY(4);
//scrittura dell'oggetto
try {
FileOutputStream f=new FileOutputStream("file_Serial.txt");
ObjectOutputStream os= new ObjectOutputStream(f);
os.writeObject(punto);
os.flush();
os.close();
}catch (IOException e) {System.exit(2);}
punto.setX(10);
punto.setY(10);
System.out.println("punto: X=" + punto.getX() + " Y=" + punto.getY() );
//lettura dell'oggetto
try {
FileInputStream f=new FileInputStream("file_Serial.txt");
ObjectInputStream is= new ObjectInputStream(f);
27
La gestione dei flussi e dei file in Java – Edoardo Ardizzone & Riccardo Rizzo
// cast alla classe letta dal file
punto=(Coordinate) (is.readObject() );
is.close();
}catch (IOException e) {System.exit(0);}
catch (ClassNotFoundException e) {System.exit(0);}
System.out.println("punto dopo la lettura:
X=" + punto.getX() +
" Y=" + punto.getY() );
}//fine main
}//fine classe
7. Esempio: creazione di un file sequenziale contenente dati relativi ad oggetti di
più classi
L’esempio illustra una applicazione che scrive su file sequenziale i risultati degli esami di una
materia di alcuni studenti. Il record studente (Matricola, Cognome, Nome, Dataesame, Votoesame,
Note) è definito dalla classe (serializzata) StudentRecord, che si avvale anche, secondo la regola
di composizione, della classe (serializzata) Date. La classe eseguibile CreateExamenFile
implementa una interfaccia GUI che consente la scelta del nome del file, attraverso la classe
JfileChooser, e l’immissione dei dati, un record alla volta. L’applicazione prevede la creazione
del package esempifile nella directory corrente. Pertanto le classi StudentRecord e Date devono
essere compilate con il comando javac –d . xxx.java.
Classe Date
// Date.java
/* La classe Date (essenzialmente è quella definita nella fig. 8.8 del testo) è
utilizzata nell'applicazione che crea file dei risultati degli esami degli studenti, in
composizione con la classe StudentRecord. */
package esempifile;
import java.io.Serializable;
public class Date
implements Serializable {
private int day;
private int month;
private int year;
// 1-31 based on month
// 1-12
// any year
// constructor: call checkMonth to confirm proper value for month;
// call checkDay to confirm proper value for day
public Date( int theDay, int theMonth, int theYear )
{
month = checkMonth( theMonth ); // validate month
day = checkDay( theDay );
// validate day
year = theYear;
// could validate year
System.out.println( "Date object constructor for date " +
toString() );
} // end Date constructor
// utility method to confirm proper month value
28
La gestione dei flussi e dei file in Java – Edoardo Ardizzone & Riccardo Rizzo
private int checkMonth( int testMonth )
{
if ( testMonth > 0 && testMonth <= 12 )
return testMonth;
// validate month
else { // month is invalid
System.out.println( "Invalid month (" + testMonth +
") set to 1." );
return 1; // maintain object in consistent state
}
} // end method checkMonth
// utility method to confirm proper day value based on month and year
private int checkDay( int testDay )
{
int daysPerMonth[] =
{ 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
// check if day in range for month
if ( testDay > 0 && testDay <= daysPerMonth[ month ] )
return testDay;
// check for leap year
if ( month == 2 && testDay == 29 && ( year % 400 == 0 ||
( year % 4 == 0 && year % 100 != 0 ) ) )
return testDay;
System.out.println( "Invalid day (" + testDay + ") set to 1." );
return 1;
// maintain object in consistent state
} // end method checkDay
// return a String of the form month/day/year
public String toString()
{
return day + "/" + month + "/" + year;
}
} // end class Date
Classe StudentRecord
// StudentRecord.java
/* Questa classe definisce il record studente (Matricola, Cognome, Nome, Dataesame,
Votoesame, Note) per il file dei risultati degli esami di una materia */
package esempifile;
import java.io.Serializable;
public class StudentRecord implements Serializable {
private int matricola;
private String cognome;
private String nome;
private Date dataEsame;
// classe Date
private int votoEsame;
// 0 - 30
private String note;
// costruttore
public StudentRecord( int matr, String cogn, String nom, Date dates, int vt,
String nt )
{
setMatricola( matr );
29
La gestione dei flussi e dei file in Java – Edoardo Ardizzone & Riccardo Rizzo
setCognome( cogn );
setNome( nom );
setDataEsame( dates );
setVotoEsame( vt );
setNote( nt );
}
// set matricola
public void setMatricola( int matr )
{
matricola = matr;
}
// get matricola
public int getMatricola()
{
return matricola;
}
// set cognome
public void setCognome( String cogn )
{
cognome = cogn;
}
// get cognome
public String getCognome()
{
return cognome;
}
// set nome
public void setNome( String nom )
{
nome = nom;
}
// get nome
public String getNome()
{
return nome;
}
} // end class
Classe CreateExamenFile
// CreateExamenFile.java
/* Questa è la classe eseguibile dell'applicazione che scrive su file sequenziale
i risultati degli esami di una materia degli studenti utilizzando la classe
serializzata StudentRecord. I record sono oggetti che vengono scritti
sequenzialmente nel file utilizzando la classe ObjectOutputStream. */
import
import
import
import
30
java.io.*;
java.awt.*;
java.awt.event.*;
javax.swing.*;
La gestione dei flussi e dei file in Java – Edoardo Ardizzone & Riccardo Rizzo
import esempifile.StudentRecord;
// importa la classe StudentRecord
import esempifile.Date;
// importa la classe Date
public class CreateExamenFile extends JFrame {
private ObjectOutputStream output;
// riferimento a file di uscita
private JLabel matrLabel, cognLabel, nomeLabel, dataLabel, votoLabel, noteLabel;
private JTextField matrField, cognField, nomeField, dataGiorno, dataMese,
dataAnno, votoField, noteField;
private JButton enterButton, saveButton, exitButton;
//enterButton per
registrare i dati inseriti, saveButton per aprire il file, exitButton per
terminare
// costruttore
public CreateExamenFile()
{
// crea GUI
super( "Creazione file esame studenti" );
// Intestazione finestra
Container c = getContentPane();
c.setLayout( new FlowLayout() );
matrLabel = new JLabel( "Matricola" );
matrField = new JTextField( 5 );
c.add( matrLabel );
c.add( matrField );
cognLabel = new JLabel( "Cognome" );
cognField = new JTextField( 10 );
c.add( cognLabel );
c.add( cognField );
nomeLabel = new JLabel( "Nome" );
nomeField = new JTextField( 10 );
c.add( nomeLabel );
c.add( nomeField );
dataLabel = new JLabel( "Data esame (gg mm aaaa)" );
dataGiorno = new JTextField( 2 ); // La data è un campo diviso in tre
sottocampi
dataMese = new JTextField( 2 );
dataAnno = new JTextField( 4 );
c.add( dataLabel );
c.add( dataGiorno );
c.add( dataMese );
c.add( dataAnno );
votoLabel = new JLabel( "Voto esame ( da 0 a 30 )" );
votoField = new JTextField( 2 );
c.add( votoLabel );
c.add( votoField );
noteLabel = new JLabel( "Annotazioni" );
noteField = new JTextField( 30 );
c.add( noteLabel );
c.add( noteField );
enterButton = new JButton( " Enter " );
enterButton.setEnabled( false ); // la prima volta disattivato
saveButton = new JButton( "Save as ..." );
exitButton = new JButton( "Exit");
exitButton.setEnabled( false ); // la prima volta disattivato
c.add( enterButton );
c.add( saveButton );
31
La gestione dei flussi e dei file in Java – Edoardo Ardizzone & Riccardo Rizzo
c.add( exitButton );
//
crea un'istanza di classe interna GestioneEventi per la gestione
dell'interazione con l'utente
GestioneEventi gestore = new GestioneEventi();
// registra i gestori degli eventi sui pulsanti
enterButton.addActionListener( gestore );
// gestore è l'action listener
saveButton.addActionListener( gestore );
exitButton.addActionListener( gestore );
} // fine costruttore
// metodo openFile
private void openFile()
{
// utilizza la classe JFileChooser per visualizzare un file dialog box che
consente all'utente di scegliere il file da aprire
JFileChooser fileWin = new JFileChooser();
fileWin.setFileSelectionMode( JFileChooser.FILES_ONLY );
fileWin.setCurrentDirectory( fileWin.getCurrentDirectory() );
partire nel file dialog box dalla directory corrente
int result = fileWin.showSaveDialog( this );
if ( result == JFileChooser.CANCEL_OPTION )
return
return;
// per
//per gestione click utente
// se l'utente clicca Cancel,
// altrimenti:
File fileName = fileWin.getSelectedFile(); // crea un oggetto File associato
al file prescelto
// visualizza eventuale messaggio di errore causato dal nome del file
if ( fileName == null || fileName.getName().equals( "" ) )
JOptionPane.showMessageDialog( this, "Invalid File Name",
"Invalid File Name", JOptionPane.ERROR_MESSAGE );
else {
// apre il file e lo riferisce da ora in poi mediante output
try {
output = new ObjectOutputStream(
new FileOutputStream( fileName ) );
System.out.println("File " + fileName.getName() + " aperto
correttamente."); // messaggio di conferma durante debug
}
// eventuali eccezioni apertura file
catch ( IOException ioException ) {
JOptionPane.showMessageDialog( this, "Error Opening File",
"Error", JOptionPane.ERROR_MESSAGE );
}
} // end else
} // end method openFile
// metodo closeFile: chiude il file e termina l'applicazione
private void closeFile()
{
32
La gestione dei flussi e dei file in Java – Edoardo Ardizzone & Riccardo Rizzo
try {
output.close();
System.exit( 0 );
}
// eventuali eccezioni chiusura file
catch( IOException ioException ) {
JOptionPane.showMessageDialog( this, "Error closing file",
"Error", JOptionPane.ERROR_MESSAGE );
System.exit( 1 );
}
} // fine metodo closeFile
// metodo addRecord
public void addRecord()
{
StudentRecord record;
// trasferisce i valori dai campi dell'interfaccia
int matricola = Integer.parseInt( matrField.getText() );
String cognome = cognField.getText();
String nome = nomeField.getText();
Date data = new Date( Integer.parseInt( dataGiorno.getText() ),
Integer.parseInt( dataMese.getText() ),
Integer.parseInt( dataAnno.getText() ) );
int voto = Integer.parseInt ( votoField.getText() );
String note = noteField.getText();
// il campo matricola deve essere riempito obbligatoriamente
if ( ! matrField.getText().equals( "" ) ) {
// scrivi un nuovo record
try {
// crea il nuovo record
record = new StudentRecord(
matricola, cognome, nome, data, voto,
note );
// output record e flush buffer
output.writeObject( record );
output.flush();
// reset campi GUI
matrField.setText( "" );
cognField.setText( "" );
nomeField.setText( "" );
dataGiorno.setText( "" );
dataMese.setText( "" );
dataAnno.setText( "" );
votoField.setText( "" );
noteField.setText( "" );
} // end try
// eventuali eccezioni scrittura record su file
catch ( IOException ioException ) {
JOptionPane.showMessageDialog( this, "Error writing to file",
"IO Exception", JOptionPane.ERROR_MESSAGE );
closeFile();
}
} // end if
33
La gestione dei flussi e dei file in Java – Edoardo Ardizzone & Riccardo Rizzo
} // end method addRecord
// main
public static void main( String args[] )
{
CreateExamenFile window = new CreateExamenFile(); //istanzia la GUI
window.setSize( 460, 140 );
window.setVisible( true );
} // end main
// dichiarazione classe interna GestioneEventi
private class GestioneEventi implements ActionListener {
// metodo actionPerformed
public void actionPerformed( ActionEvent e )
{
if ( e.getSource() == saveButton ) // tasto "Save as ..."
{
while ( output == null ) openFile(); //crea file (rimane nel ciclo
finché si seleziona un nome valido di file)
enterButton.setEnabled( true); // attiva pulsanti enter e exit
exitButton.setEnabled( true); //
saveButton.setEnabled( false ); // disattiva pulsante save as
}
else if ( e.getSource() == enterButton ) // tasto "Enter"
addRecord(); // aggiunge un record al file
else if ( e.getSource() == exitButton ) // tasto "Exit"
closeFile(); // chiude il file e termina l'applicazione
} // fine actionPerformed
} // fine classe interna GestioneEventi
} // fine classe
CreateExamenFile
Riferimenti bibliografici
1. http://java.sun.com/docs/books/tutorial/essential/.
2. C. S. Horstmann, Concetti di Informatica e Fondamenti di Java (Terza edizione), 2005,
Apogeo, capp. 14 e 15.
3. Deitel & Deitel, Java – Tecniche avanzate di programmazione (Seconda edizione), 2003,
Apogeo, capp. 4 e 6.
4. G. Cabri & F. Zanbonelli – Programmazione a oggetti in Java: dai Fondamenti a Internet,
2003, Pitagora Editrice Bologna, cap. 4.
34