ATTENZIONE: il nuovo sito di riferimento per questa guida e altre cose più o meno interessanti è il mio BLOG

Indice
Prossimo capitolo - Programmazione concorrente

Capitolo 7 - Leggere e scrivere file

In java.io ci sono due gruppi principali di classi, uno che lavora con i byte, e uno che gestisce i caratteri unicode.
Queste classi sono abbastanza generiche da permettere di lavorare con molte entità diverse - file, connessioni di rete, buffer in memoria, pipe, ecc. - anche se in questo capitolo ci si concentrerà sui file.

Scanner

Il modo più semplice per leggere un file di testo è quello di usare la nuova (da java 1.5) classe java.util.Scanner.
Come esempio il seguente programma stampa a video il file di testo passato come primo argomento:

	import java.io.*;
	import java.util.*;

	class JCat
	{
		public static void main(String[] args)
			throws Exception	// ATTENZIONE!
		{
			Scanner sc = new Scanner(new File(args[0]));
			sc.useDelimiter("\n");

			while(sc.hasNext())
				System.out.println(sc.next());
		}
	}
Scanner è un Iterator, come si può vedere dalle ultime due linee.

RandomAccessFile

Un'altra classe un po' "alternativa" è java.io.RandomAccessFile, molto più familiare delle classi "canoniche" per chi proviene dal C, e adatta ad aprire file in lettura e scrittura ("rw") o in sola lettura ("r"):

	RandomAccessFile f =
		new RandomAccessFile("file.txt", "rw");

	f.writeUTF("File ad accesso casuale.\n");
	f.seek(0);	// va all'inizio
	f.writeUTF("abcd");	// sostituisce 'File' con 'abcd'
	f.close();

Reader e Writer

Reader e Writer sono al vertice di due gerarchie di classi che si occupano rispettivamente della lettura e della (sovra)scrittura di char.Partiamo direttamente con un esempio, la lettura e stampa del file "nomefile.txt":

	String s;
	BufferedReader reader =
		new BufferedReader(
			new FileReader("nomefile.txt") );
5
	while( (s = reader.readLine()) != null )
		System.out.println(s);

	reader.close();
Con le linee 2-4 è stato creato un oggetto FileReader a partire dal nome del file da leggere, che a sua volta viene usato per creare un BufferedReader. Questo "wrap" serve ad attivare una cache, che permette un notevole incremento di prestazioni.

Vediamo ora l'output:

	PrintWriter writer =
		new PrintWriter(
			new BufferedWriter(
				new FileWriter("nomefile.txt",
					true))); // non sovrascrivere, accoda

	writer.print("Questo ");
	writer.println("e' un file di testo.");
In questo caso si è aperto il file in "append" (java 1.4+). È stato usato PrintWriter per facilitare la scrittura del testo.

InputStream e OutputStream

Quando è necessario lavorare con i byte le classi derivate da Reader e Writer non sono più adatte: servono quelle delle famiglie InputStream e OutputStream (oppure, solo per i file, RandomAccessFile).

I nomi delle classi e il loro funzionamento sono tuttavia molto simili - ad esempio la classe corrispondente a FileReader è FileInputStream. Esistono anche delle classi per la conversione, in una direzione:

	reader = new InputStreamReader(is);
	writer = new OutputStreamWriter(os);
Due classi non hanno corrispondenti nella famiglia "char oriented", DataInputStream e DataOutputStream:
	DataOutputStream do =
		new DataOutputStream(
			new BufferedOutputStream(
				new FileOutputStream("nomefile.dat",
5					true))); // append

	do.writeInt(43210);
	do.close();
In questo pezzo di codice viene aperto un file su cui viene scritto il valore dell'intero 43210 (4 byte).

Similmente si lavora con DataInputStream.

Standard input

System.in è un InputStream collegato allo standard input. Vediamo allora come ottenere dall'utente un numero intero immesso da tastiera:

	Scanner stdin =
		new Scanner(System.in);

	int i;

	System.out.print("Inserisci un intero: ");

	while(true)
		try {
			i = stdin.nextInt();
			break;		// niente eccezione, abbiamo il numero
		} catch(InputMismatchException e) {
			System.out.println("Devi inserire un int!");
			stdin.next();	// "scarta" l'elemento
		}

	System.out.println("Hai immesso l'intero: " + i);
Attenzione, in Italia il punto è un LocalGroupSeparator - vedi java.text.DecimalFormatSymbols.getGroupingSeparator() - che separa le migliaia: 123.456 diventa così un int, non un float!

L'immissione di stringhe è ancora più semplice:

	Scanner stdin = new Scanner(System.in);
	stdin.useDelimiter("\n");	// prendiamo tutta la riga

	System.out.print("Inserisci il tuo nome: ");
	String s = stdin.next();

	System.out.println("Il tuo nome e': " + s);

Comprimere gli stream

Un OutputStream si può comprimere in modo trasparente, passandolo al costruttore di java.util.zip.GzipOutputStream:

	BufferedOutputStream out=
		new BufferedOutputStream(
			new GzipOutputStream(
				// attenzione, sovrascrive!
				new FileOutputStream("out.dat")));
Per leggere da uno stream compresso con GZIP si può usare GzipInputStream.

Esistono anche classi analoghe per la gestione di file in formato ZIP.

La classe File

La classe java.io.File ha un nome forse fuorviante, dato che non rappresenta un file, ma un percorso - chiamarla "Path" sarebbe stato forse più appropriato. Una volta creato un oggetto File, ad esempio con:

	File f = new File(args[0]);
posso usarlo per sapere se tale percorso esiste:
	System.out.println("Il file " + args[0] +
			( f.exists() ? "" : " non") +
			" esiste");
oppure creare la gerarchia di directory nel percorso:
	f.mkdirs();
eccetera.

Salvare oggetti

Supponiamo di dover scrivere nel file "out.dat" un'istanza della seguente classe - notate che per usare questo meccanismo è necessario implementare Serializable:

	class OggettoStrano
		implements Serializable
	{
		String nome;
		transient String pass;
		...
	}
Nota: se si desidera che alcuni campi dell'oggetto non vengano salvati, magari perché contenenti dati sensibili o che non ha senso salvare, bisogna marcarli come transient.

Potremmo procedere così:

	OggettoStrano o = new OggettoStrano();

	ObjectOutputStream oos =
		new ObjectOutputStream(
			new BufferedOutputStream(
				new FileOutputStream("out.dat")));

	// String e' Serializable
	oos.writeObject("Questo file contiene un oggetto strano.");

	oos.writeObject(o);	// scrivo l'oggetto
	oos.close();		// chiudo lo stream
Come vedete nello stesso stream si possono salvare più oggetti in sequenza - in questo caso una stringa e l'oggetto in questione. Per recuperare questi oggetti al solito si usa la classe simmetrica, ObjectInputStream, e il metodo readObject(), il quale restituisce un Object, su cui si dovrà eseguire il cast appropriato.

Il meccanismo di serializzazione è usato, oltre che per ottenere la persistenza di oggetti, anche per passarli tra diverse JVM, magari dislocate su macchine diverse su una rete, ad esempio usando Java RMI.



Indice
Prossimo capitolo - Programmazione concorrente