Classi per lettura e scrittura di file

Nei programmi sviluppati sino ad oggi abbiamo usato 
  • output su schermo (con System.out.print, System.out.println e System.out.printf)

  • input da tastiera (con la classe Input)

Se un programma deve leggere o scrivere grandi quantità di dati, è conveniente memorizzare questi dati in files. 

Java fornisce classi e metodi per scrivere e leggere dati da files (nel package java.io  [locale, Medialab, Sun]).

I dati possono essere memorizzati in un file in formato

  • testo (sequenza di caratteri, leggibile da esseri umani)

  • binario (sequenza di byte)

Classi per I/O su files Input da file Output su file
Formato testo FileReader FileWriter
Formato binario FileInputStream FileOutputStream

Queste classi gestiscono I/O di caratteri o bytes da files: per dati più complessi (stringhe, numeri) introdurremo altre classi.

Adesso esaminiamo I/O in formato testo, ma quello in formato binario è del tutto analogo.

I file di testo possono essere aperti, esaminati e modificati usando normali editor (es. emacs). Per visualizzarne il contenuto potete anche usare il comando less della shell di Linux.
 
 

 




Lettura di singoli caratteri da file

Per leggere dati (un carattere alla volta) da un file, occorre:
  • ricordarsi di importare il pacchetto java.io

    import java.io.*;
    

  • creare un oggetto della classe FileReader, passando il nome del file al costruttore; 

        FileReader filein = new FileReader("dati.txt"); 
    

  • utilizzare (anche più volte) il metodo  public int read() della classe FileReader per leggere i caratteri;

        int codiceCarattere = filein.read();
    

  • infine chiudere il file con il metodo public void close().

        filein.close();
    

I metodi e il costruttore di FileReader possono lanciare una eccezione di tipo IOException  che rappresenta un errore di I/O. Queste eccezioni sono controllate, e quindi devono essere previste dal programmatore.

Il metodo read() restituisce un intero che può essere:

  • -1 se si è arrivati alla fine del file;

  • un intero tra 0 e 16383, che rappresenta il codice di un carattere UNICODE.
Tipicamente, si controlla se il numero letto è diverso da -1 e in questo caso si trasforma l'intero in un char usando l'operatore di cast.



Esempio di lettura da file


CopyRead.java: stampa su video del contenuto di un file il cui nome viene chiesto all'utente (ad esempio, copyread.txt). Le eventuali IOException vengono catturate e in tal caso viene stampato un messaggio di errore.
 
import java.io.*;

public class CopyRead {
    public static void main(String[] args) {
        System.out.print("Nome file (senza suffisso): ");
        String nome = Input.readLine();
        
        try {
            // apre il file in lettura
            FileReader filein = new FileReader(nome + ".txt");
            
            int next;
            do {
                next = filein.read(); // legge il prossimo carattere
                
                if (next != -1) { // se non e' finito il file
                    char nextc = (char) next;
                    System.out.print(nextc); // stampa il carattere
                }

            } while (next != -1);
            
            filein.close(); // chiude il file
            
        } catch (IOException e) {
            System.out.println(e);
        }
        System.out.println("\nBye bye!");
    }
}

Alternativamente si poteva delegare la gestione delle IOException al chiamante, aggiungendo la clausola throws IOException nell'intestazione del metodo main.




Scrittura di caratteri su file

Per scrivere dati su di un file, occorre:
  • creare un oggetto della classe FileWriter, passando il nome del file al costruttore; 

  • utilizzare il metodo public void write(int c) per scrivere i caratteri;

  • chiudere il file con il metodo close().

Anche questi metodi possono lanciare una IOException.

CopyWrite.java: scrive in un file una sequenza di stringhe fornita dall'utente, un carattere alla volta.
 

import java.io.*;

public class CopyWrite {
    public static void main(String[] args) {
        System.out.print("Nome file (senza suffisso): ");
        String nome = Input.readLine();
        try {
            
            // apre il file in scrittura
            FileWriter fileout = new FileWriter(nome + ".txt");
            String str;
            do {
                System.out.print("Scrivi una stringa (vuota per terminare): ");
                // legge una stringa da tastiera
                str = Input.readLine();
                
                // il ciclo scrive ogni carattere delle stringa nel file
                for (int i = 0; i < str.length(); i++)
                    fileout.write(str.charAt(i));
                fileout.write('\n');
                
            } while (str.length() > 0);
            
            fileout.close(); // chiude il file
            
        } catch (IOException e) {
            System.out.println(e);
        } 
        System.out.println("\nBye bye!");
    }
}

Si osservi che se si verifica un'eccezione mentre si tenta di scrivere un carattere nel file, il metodo close() non viene eseguito: in certe situazioni questo potrebbe portare alla perdita dei dati scritti nel file.
Il programma CopyWriteFinally.java [CopyWriteFinally.html] mostra come si può usare la clausola finally del try-catch per garantire che il file venga chiuso: si osservi che in questo caso il main deve necessariamente avere la clausola throws.




I/O bufferizzato e formattato

Le classi FileReader e FileWriter forniscono i metodi basici per leggere o scrivere caratteri su file. Non è conveniente usarle direttamente nei programmi perché non permettono di leggere/scrivere direttamente dati più complessi come stringhe e numeri.

Altre classi di Java forniscono funzionalità di I/O più avanzate, in particolare

  • BufferedReader e BufferedWriter usano un buffer (memoria tampone) per memorizzare temporaneamente i caratteri da leggere/scrivere, in modo da ridurre il numero di accessi al file;

  • PrintWriter fornisce i metodi print e println, che permettono di scrivere qualunque dato Java, convertendolo automaticamente in stringa.

Gli oggetti di queste classi sono dei wrappers: incapsulano gli oggetti delle classi FileReader e FileWriter estendendone le funzionalità.



Scrittura di dati su file

CopyPrintWrite.java: scrive alcuni dati in un file (copyprintwrite.txt). 
Si noti come un oggetto FileWriter viene passato al costruttore di BufferedWriter, e a sua volta il nuovo oggetto viene passato al costruttore di PrintWriter
 
import java.io.*;

public class CopyPrintWrite {
    public static void main(String[] args) throws IOException {
        
        // creo un oggetto FileWriter...
        FileWriter fileout = new FileWriter("copyprintwrite.txt");
        // ... che incapsulo in un BufferedWriter...
        BufferedWriter filebuf = new BufferedWriter(fileout);
        // ... che incapsulo in un PrintWriter
        PrintWriter printout = new PrintWriter(filebuf);
        
        printout.println("Scrivo nel file copyprintwrite.txt:");

        printout.print("Un numero: ");
        printout.println(Math.PI);

        printout.print("Un oggetto: ");
        printout.println(new java.awt.Rectangle(10,15,20,30));

        printout.println("Ho finito.");
        printout.close();
        System.out.println("\nBye bye!");
    }
}

 



Lettura di dati da file

La classe BufferedReader fornisce il metodo readLine() che legge una stringa, ma non ha metodi per leggere, ad esempio, interi o double

CopyBufferedRead.java: stampa su video il contenuto di un file (copyread.txt) (si confronti con CopyRead.java)
 

import java.io.*;

public class CopyBufferedRead {
    public static void main(String [] args) throws IOException {

        // incapsula in BufferedReader un file aperto in lettura 
        BufferedReader filebuf = 
             new BufferedReader(new FileReader("copyread.txt")); 
        /* equivale alla coppia di istruzioni:
         *
         * FileReader filein = new FileReader("copyread.txt");
         * BufferedReader filebuf = new BufferedReader(filein);
         */

        String nextStr;
        nextStr = filebuf.readLine();     // legge una riga del file 
        while (nextStr != null){
            System.out.println(nextStr);  // visualizza la riga 
            nextStr = filebuf.readLine(); // legge la prossima riga 
        } 
        filebuf.close();  // chiude il file 
        System.out.println("\nBye bye!");
    }
}

Si può convertire una stringa in un numero usando i metodi statici Integer.parseInt e Double.parseDouble. Si veda come questi metodi sono usati nella classe Input, per realizzare i metodi readInt() e readDouble().

Nota: Integer.parseInt e Double.parseDouble possono generare eccezioni non controllate (di tipo NumberFormatException).




Esempio: somma di interi da file

Il programma SommaInteriGoodExceptions.java, stampa la somma di una sequenza di interi, contenuti uno per linea nel file integers.txt, con una ragionevole gestione delle eccezioni.
import java.io.*;

public class SommaInteriGoodExceptions { 
    
    // programma che legge numeri da file, 
    // uno per riga,  e ne stampa la somma
    
    public static void main(String[] args){
        
        String inputFileName = "./integers.txt";
        String line = null;
            
        try {
            BufferedReader in = 
                new BufferedReader(new FileReader(inputFileName));
            
            line = in.readLine();
            int somma =0;
            
            while(line!=null){
                // se non e' possibile convertire la stringa line in
                // intero viene lanciata un'eccezione
                // NumberFormatException (catturata sotto con catch
                // apposito)
                int numero = Integer.parseInt(line);

                somma += numero;
                line = in.readLine();
            }
            in.close();
            System.out.println(" la somma e'  "+ somma);
            
        } catch(FileNotFoundException e) {
            System.out.println(inputFileName+" FileNotFound");
        } catch(NumberFormatException e) {
            System.out.println(" linea non corretta: -> "+line+" <-");
        } catch(IOException e) {
            System.out.println(" IOException  "+e);
        }
        System.out.println(" fine esecuzione ");
    }
}

Due semplici esercizi:

  1. Modificare il programma SommaInteriGoodExceptions in modo che quando si verifica un'eccezione venga comunque stampata la somma calcolata fino a quel momento.

  2. Modificare il programma SommaInteriGoodExceptions in modo che quando si verifica un'eccezione di tipo NumberFormatException dovuta ad una linea del file non convertibile in intero si scarti tale riga ma si prosegua con il calcolo della somma.



Esempi di gestione delle eccezioni di I/O

Abbiamo visto che le IOException sono eccezioni controllate, e quindi devono essere catturate o delegate al chiamante. Vediamo alcuni programmi che copiano il contenuto del file inp.txt nel file out.txt, ma che gestiscono le eccezioni in modo via via più raffinato.