archiviareSuDisco

leggere e scrivere informazioni su memoria permanente

Il problema

La nostra squadra di atletica ha bisogno tener conto dei tempi migliori degli atleti che compongono la squadra di staffetta 4x400 e di poter calcolare il tempo totale. L'allenatore vuole che i tempi vengano memorizzati da una sesione di allenamento all'altra per poter fare statistiche.
Purtroppo i programmi che abbiamo scritto finora perdono tutte le informazioni una volta chiusi, vorremmo adesso invece imparare a registrare le informazioni. Questo può essere fatto archiviando le informazioni (ci concentriamo sono i file di test) "su disco".

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 uno 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:

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());

Leggere/scrivere bytes

Java ci mette a disposizione molti oggetti nel pacchetto java.io per poter leggere o scrivere informazioni su disco: gli Stream sono oggetti che permettono di lavorare con dei flussi di byte, ne esistono molti diversi e si possono distinguere dal loro nome due grandi categorie: gli InputStream (che servono per leggere) e gli OutputStream (che servono per scrivere).

Una volta che si sono completate le operazioni di output (e anche di input) questi oggetti vanno chiusi utilizzando il metodo close() 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 classe chiamata ObjectInputStream serve a:

scrivere oggetti su discoin questo caso avrebbe avuto "Output" nel nome leggere oggetti da uno Stream

Qui 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);
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

Queste due categorie di oggetti ci consentono di leggere/scrivere dei caratteri e possiamo crearli o partendo da uno Stream o usando una scorciatoia...

Il primo esempio che vediamo usa un OutputStreamWriter per scrivere "ciao" in un file di testo:

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 sceglie altre come ad esempio "ISO8859-1" che è quella che usana Windows per default, 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).

Siccome normalmente i dati si scrivono su un file su disco il programma sopra può essere abbreviato in:

FileWriter flussoCaratteri = new FileWriter("c:\\Users\\pluto\\esempio.txt");
flussoCaratteri.write("è");
flussoCaratteri.close();

In questo caso però non possiamo specificare l'encoding e quindi verrà utilizzato quello predefinito sul sistema in uso (Windows e macOS hanno due default diversi).

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");
char caratteri[] = new char[1000];
int caratteriLetti = flussoCaratteri.read(caratteri);
String testo = new String(caratteri,0,caratteriLetti);
System.out.println(testo);
flussoCaratteri.close();

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 vuiol 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.

Dalle ultime versioni di java è possibile anche creare un FileReader specificando il persorso e l'encoding, la sintassi è un pochino più complicata ma non tanto:

FileReader flussoCaratteri = new FileReader("percorso.txt",Charset.forName("UTF-8"))

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 capoda 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 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.

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.

Al posto del ciclo while nel programma sopra scrivo questo:

for(testo=lettoreDiRighe.readLine(); testo!=null; testo=lettoreDiRighe.readLine()){
  gruppo = gruppo + " " + testo;
}
nodevi rileggere il testo con più attenzione... o forse il ciclo for si