Il problema
I programmi che abbiamo scritto finora perdono tutte le informazioni una volta chiusi, vorremmo adesso invece imparare a registrare le informazioni in maniera permanente su disco, per fare questo ci concentreremo in particolare sui file di testo.
Il percorso più "flessibile" che possiamo usare con java (ma esistono delle scorciatorie che vedremo) è di creare un oggetto che rappresenta il file, in base a questo crearne un altro che ci consente di leggere i byte da disco (perché su disco questo c'è!) e poi crearne uno ulteriore che trasforma quei byte in caratteri... e volendo ancora uno in più per leggere direttamente le linee!
Gli oggetti che useremo in questo capitolo sono contenuti nel pacchetto java.io
Percorsi
Per registrare e leggere informazioni è necessario interagire con in file system: la prima cosa che dobbiamo fare è specificare la posizione del file nel sistema di archiviazione. Sistemi operativi diversi descrivono in maniera diversa la posizione di un file: Windows per separare le cartelle che compongono il percorso usa "\" mentre Unix (ad esempio Linux e MacOS) usa "/", il primo specifica il disco usando una lettera seguita da due punti (es: "C:") mentre il secondo indica un percorso assoluto usando un "/" all'inizio del percorso.
Oltre a questo se trattiamo percorsi assoluti (cioè quelli che iniziano con una lettera su Windows o con uno slash su Unix) che contengono il nome della cartella utente questi cambiano per ciascuno di noi.
Prendiamo per esempio il percorso del file saluti.txt
che sta
nella cartella documenti di un utente.
per l'utente Matteo su Windows è C:\Users\matteo\Documents\saluti.txt
per l'utente Luca su Linux è /home/luca/Documenti/saluti.txt
Attenzione a due cose:
- alcuni filesystem sono case sensitive
- le interfacce grafiche a volte mostrano le cartelle con altro nome (es: "Scrivania" ➞ "Desktop")
L'esempio qui sotto usa l'oggetto File
e stampa true o false sulla console a seconda che il file il
cui percorso assoluto è "c:/Users/pluto/esempio.txt" esista o meno.
File mioFile = new File("c:/Users/pluto/esempio.txt");
System.out.println(mioFile.exists());
Operazioni comuni
Le classi per leggere/scrivere informazioni su disco che andremo ad usare
usano un meccanismo comune:
il file viene aperto quando si crea l'oggetto (usando new
) e va chiuso
usando il metodo close()
dell'oggetto stesso sia per
liberare risorse di sistema che per far si che eventuali operazioni non ancora completate vengano
completate (ad esempio se l'oggetto usa dei buffer non scrive immediatamente su disco).
La possibilità di leggere o scrivere su un file è determinata dall'oggetto che si usa e facilmente riconoscibile dal fatto che il nome della classe contenga la parola Read o Input o Write o Output.
Leggere/scrivere bytes
Tutti i dati memorizzati su disco sono in pratica una sequenza di byte che viene poi interpretata nel modo opportuno dopo la lettura (es: se leggo un file jpeg i byte che recupero dal disco li interpreto come immagine).
Iniziamo dagli Stream che sono appunto oggetti che permettono di lavorare direttamente sui byte, ne esistono molti diversi e si possono distinguere dal loro nome in due grandi categorie: gli InputStream (che servono per leggere) e gli OutputStream (che servono per scrivere).
La classe chiamata ObjectInputStream serve a:
scrivere oggetti su discoin questo caso avrebbe avuto "Output" nel nome leggere oggetti da uno StreamQui sotto un frammento di programma che scrive un byte in un file.
File mioFile = new File("c:\\Users\\pluto\\esempio.txt");
FileOutputStream flussoBytes = new FileOutputStream(mioFile);
byte b = (byte) 45;
flussoBytes.write(45);
flussoBytes.close();
Mentre questo legge un byte da disco:
File mioFile = new File("c:\\Users\\pluto\\esempio.txt");
FileInputStream flussoBytes = new FileInputStream(mioFile);
int n = flussoBytes.read();
System.out.println(n);
flussoBytes.close();
Due cose vanno notate:
- leggere un byte alla volta è praticamente inutile per noi che vogliamo leggere un testo
- è possibile creare un FileOutputStream o FileInputStream in maniera più
semplice fornendo direttamente il suo nome al costruttore
FileOutputStream flussoBytes = new FileOutputStream("c:\\Users\\pluto\\esempio.txt")
.
Leggere/scrivere caratteri
Esistono diverse classi per questo scopo, ne vediamo quattro che differiscono soltanto per il modo usato per creare l'oggetto. Iniziamo dalla scrittura di un testo su file: possiamo creare un Writer partendo da uno Stream oppure direttamente dal percorso del file. Vediamo un esempio della prima modalità:
FileOutputStream flussoBytes = new FileOutputStream("c:\\Users\\pluto\\esempio.txt");
OutputStreamWriter flussoCaratteri = new OutputStreamWriter(flussoBytes,"UTF-8");
flussoCaratteri.write("ciao");
flussoCaratteri.close();
flussoBytes.close();
Un oggetto di tipo OutputStreamWriter
è in grado di scrivere dei testi su disco usando
una precisa codifica, in questo caso abbiamo scelto "UTF-8" ma se ne possono scegliere altre come
ad esempio "ISO8859-1", ma cosa cambia da una
codifica all'altra? Il modo con cui vengono scritti i byte su disco: se uso UTF-8 una "è"
viene scritta come due byte: "195, 168" mentre se uso "ISO8859-1" viene scritto "232",
discorso analogo ovviamente vale in lettura. Quando leggo o scrivo dei dati in formatio testo
è necessario che io sappia quale regola di codifica va usata (decide chi scrive il file
la prima volta).
Vediamo un esempio della seconda modalità:
FileWriter flussoCaratteri = new FileWriter("c:\\Users\\pluto\\esempio.txt", Charset.forName("UTF-8"));
flussoCaratteri.write("è");
flussoCaratteri.close();
La classe Charset
si trova nel pacchetto java.nio.charset
In entrambi i casi se l'encoding non viene specificato viene usato UTF-8 per default.
Proviamo invece a leggere un testo da un file. Posso usare un
InputStreamReader
che a sua volta usa un FileInputStream
oppure usare
un FileReader
direttamente: vediamo la soluzione più breve.
FileReader flussoCaratteri = new FileReader("c:\\Users\\pluto\\esempio.txt");
// oppure, in caso si voglia specificare l'encoding:
// FileReader flussoCaratteri = new FileReader("percorso.txt",Charset.forName("UTF-8"))
char caratteri[] = new char[1000];
int caratteriLetti = flussoCaratteri.read(caratteri);
String testo = new String(caratteri,0,caratteriLetti);
System.out.println(testo);
flussoCaratteri.close();
Siccome l'encoding nella creazione del FileReader
non è stato specificato
i byte presenti su disco verranno tradotti in caratteri utilizzando UTF-8 come
schema di codifica.
Come si vede dalla prima riga evidenziata sopra quel che possiamo leggere da un reader è un
elenco di caratteri, devo quindi fornire al metodo read()
un vettore in
cui inserire i dati, il metodo stesso leggerà quanti dati può (non è dato sapere quanti) fino al
massimo a riempiere il vettore, restituirà comunque il numero dei caratteri letti
(se restituisce -1 vuol dire che ha raggiunto fa fine dell'archivio).
La seconda riga evidenziata serve a costruire una stringa prendendo i dati dal vettore
caratteri
, partendo dalla posizione 0
e prendendo
caratteriLetti
caratteri.
Leggere/scrivere righe di testo
Siamo finalmente arrivati al nostro obiettivo: leggere una riga (cioè una sequenza
di caratteri terminata da un "a capo") alla volta!
Ci viene in aiuto un oggetto chiamato BufferedReader
che basandosi su
un altro Reader (ad esempio un FileReader
) ci permette di leggere una riga
intera e non un vettore di caratteri. Vediamo subito un esempio di un programma
che legge la prima riga di un file.
FileReader flussoCaratteri = new FileReader("c:\\Users\\pluto\\esempio.txt");
BufferedReader lettoreDiRighe = new BufferedReader(flussoCaratteri);
String testo = lettoreDiRighe.readLine();
System.out.println(testo);
lettoreDiRighe.close();
flussoCaratteri.close();
Scrivere una riga alla volta si può fare semplicemente usando un FileWriter (o un OutputStreamWriter) in questo modo:
String testo="Questa è una riga";
FileWriter flussoCaratteri = new FileWriter("c:\\Users\\pluto\\esempio.txt");
flussoCaratteri.write(testo+"\n");
flussoCaratteri.close();
Quando scriverò i dati su disco usando un FileWriter quale carattere indicherà "a capo"?
non serve, va a capo da seno, non lo fa le virgolette chiuseno, indicano semplicemente la fine della stringa "\n"giusto!Leggere tutte le righe
il metodo readLine()
di BufferedReader
ritorna una riga alla volta
fino a quando raggiunge la fine del file, in quel caso ritorna null
, questo
ci permette di scrivere un ciclo che legga tutte le righe di un file:
FileReader flussoCaratteri = new FileReader("c:\\Users\\pluto\\esempio.txt");
BufferedReader lettoreDiRighe = new BufferedReader(flussoCaratteri);
String rigaLetta;
do {
rigaLetta = lettoreDiRighe.readLine();
if(rigaLetta!=null) {
// uso la riga letta
System.out.println(rigaLetta);
}
}while(rigaLetta!=null);
lettoreDiRighe.close();
flussoCaratteri.close();
Torna più comodo usare un ciclo a controllo finale (do-while) perché prima leggo la riga,
in caso contenga qualcosa faccio quel che devo e poi se ho raggiunto la fine del file
(e quindi il valore letto è null
) smetto di leggere.
Accodare informazioni ad un file esistente
Le modalità viste fino ad ora per scrivere sui file hanno un comportamento a volte indesiderato: iniziano a scrivere dal primo byte (o carattere) del file. Come se non bastasse se il file conteneva 1000 caratteri e dopo averlo aperto ne scriviamo soltanto 10 alla chiusura il file conterrà soltanto quei 10 caratteri.
Esiste però un modo per aprire un file per aggiungere contenuto
(in fondo ovviamente!) e non per sovrascriverlo, questa modalità
di lavorare è nota come "append mode" e usando un FileWriter
si può attivare così:
FileWriter scrittore = new FileWriter("nomeDelFile", true)
Quel true
in fondo è quello che attiva l'append-mode.
Solitamente il ciclo per leggere le righe da un file si scrive in un altro modo che a prima vista potrebbe sembrare meno comprensibile ma è più comdo:
while( (rigaLetta = lettoreDiRighe.readLine())!=null ) {
System.out.println(rigaLetta);
}
Direttamente nella condizione del while assegno a rigaLetta
il valore letto dal file e siccome l'assegnazione per java è una espressione posso
guardare se il valore assegnato è null
.