Leggere e Scrivere File in Java

Concetti Chiave
  • In Java, per leggere e scrivere file, si utilizzano le classi FileInputStream e FileOutputStream per gli stream di byte.
  • Per leggere da un file, si utilizza il metodo read() di FileInputStream, che restituisce un byte alla volta.
  • Per scrivere su un file, si utilizza il metodo write() di FileOutputStream, che scrive un byte alla volta.
  • È importante chiudere i file dopo l'uso per liberare le risorse di sistema, utilizzando il metodo close().

Lettura e Scrittura di File

Java fornisce una serie di classi e metodi che consentono di leggere e scrivere file.

Prima di iniziare, è importante affermare che l'argomento dell'I/O su file è piuttosto ampio e l'I/O su file viene esaminato in dettaglio in una serie di lezioni successive.

Lo scopo di questa sezione è introdurre le tecniche di base per leggere da e scrivere su un file. Sebbene vengano utilizzati stream di byte, queste tecniche possono essere adattate ai stream basati su caratteri.

Due delle classi di flusso più utilizzate sono FileInputStream e FileOutputStream, che creano stream di byte collegati ai file.

Per aprire un file, è sufficiente creare un oggetto di una di queste classi, specificando il nome del file come argomento al costruttore. Sebbene entrambe le classi supportino costruttori aggiuntivi, le seguenti sono le forme che utilizzeremo:

FileInputStream(String nomeFile) throws FileNotFoundException
FileOutputStream(String nomeFile) throws FileNotFoundException

Qui, nomeFile specifica il nome del file che si desidera aprire. Quando si crea un flusso di input, se il file non esiste, viene lanciata FileNotFoundException.

Per i stream di output, se il file non può essere aperto o creato, viene lanciata FileNotFoundException. FileNotFoundException è una sottoclasse di IOException. Quando un file di output viene aperto, qualsiasi file preesistente con lo stesso nome viene distrutto.

Quando si è finito con un file, è necessario chiuderlo. Questo viene fatto chiamando il metodo close(), che è implementato sia da FileInputStream che da FileOutputStream. È mostrato qui:

void close() throws IOException

Chiudere un file rilascia le risorse di sistema allocate al file, permettendo loro di essere utilizzate da un altro file. La mancata chiusura di un file può risultare in perdite di memoria (o memory leak) a causa di risorse inutilizzate che rimangono allocate.

Il metodo close() è specificato dall'interfaccia AutoCloseable in java.lang. AutoCloseable è ereditata dall'interfaccia Closeable in java.io. Entrambe le interfacce sono implementate dalle classi di flusso, incluse FileInputStream e FileOutputStream.

Prima di procedere, è importante sottolineare che ci sono due approcci di base che si possono utilizzare per chiudere un file quando si è finito con esso.

Il primo è l'approccio tradizionale, in cui close() viene chiamato esplicitamente quando il file non è più necessario. Questo è l'approccio utilizzato da tutte le versioni di Java precedenti a JDK 7 ed è, quindi, presente in tutto il codice legacy pre-JDK 7.

Il secondo è utilizzare l'istruzione try-with-resources aggiunta da JDK 7, che chiude automaticamente un file quando non è più necessario. In questo approccio, non viene eseguita nessuna chiamata esplicita a close(). Poiché si potrebbero ancora incontrare codici legacy pre-JDK 7, è importante conoscere e comprendere l'approccio tradizionale. Inoltre, l'approccio tradizionale potrebbe ancora essere il migliore in alcune situazioni. Pertanto, inizieremo con esso. L'approccio automatizzato è descritto nella prossima lezione.

Lettura da un File

Per leggere da un file, si può utilizzare una versione di read() che è definita all'interno di FileInputStream. La versione del metodo che utilizzeremo è mostrata qui:

int read() throws IOException

Ogni volta che viene chiamato questo metodo, esso legge un singolo byte dal file e restituisce il byte come valore intero. read() restituisce –1 quando viene fatto un tentativo di lettura alla fine del flusso. Può lanciare una IOException.

Il seguente programma utilizza read() per leggere e visualizzare il contenuto di un file che contiene testo ASCII. Il nome del file è specificato come argomento della riga di comando.

/*
 * Visualizza un file di testo.
 * Per utilizzare questo programma, specificare il nome
 * del file che si desidera vedere.
 * Ad esempio, per vedere un file chiamato TEST.TXT,
 * utilizzare la seguente riga di comando.
 * java MostraFile TEST.TXT
 */

import java.io.*;

class MostraFile {

    public static void main(String[] args) {
        int i;
        FileInputStream finput;

        // 1. Conferma che un nome file è stato specificato
        //    come argomento della riga di comando.
        if (args.length != 1) {
            System.out.println("Uso: MostraFile nomefile");
            return;
        }

        // 2. Tenta di aprire il file specificato
        //    creando un oggetto FileInputStream.
        //    Se il file non può essere aperto,
        //    mostra un messaggio di errore
        //    e termina il programma.
        try {
            finput = new FileInputStream(args[0]);
        } catch (FileNotFoundException e) {
            System.out.println("Impossibile Aprire File");
            return;
        }

        // 3. A questo punto, il file è aperto e può essere letto.
        //    Questa parte legge il file byte per byte
        //    e visualizza ogni byte sulla console.
        try {
            do {
                i = finput.read();
                if (i != -1)
                    System.out.print((char) i);
            } while (i != -1);
        } catch (IOException e) {
            System.out.println("Errore Lettura File");
        }

        // 4. Chiude il file
        try {
            finput.close();
        } catch (IOException e) {
            System.out.println("Errore Chiusura File");
        }
    }

}

Per lanciare il programma, bisogna specificare il nome del file da visualizzare come argomento della riga di comando. Ad esempio, per visualizzare un file chiamato test.txt, si può utilizzare la seguente riga di comando:

java MostraFile test.txt

Nel programma, si notino i blocchi try/catch che gestiscono gli errori di I/O che potrebbero verificarsi.

Ogni operazione di I/O è monitorata per le eccezioni, e se si verifica un'eccezione, viene gestita. Si tenga presente che in programmi semplici o codice di esempio, è comune vedere le eccezioni di I/O semplicemente lanciate fuori da main(), come è stato fatto nei precedenti esempi di I/O su console. Inoltre, in codice del mondo reale, può essere utile lasciare che un'eccezione si propaghi a una routine chiamante per far sapere al chiamante che un'operazione di I/O è fallita. Tuttavia, la maggior parte degli esempi di I/O su file in questa guida gestisce tutte le eccezioni di I/O esplicitamente, come mostrato, a scopo illustrativo.

Sebbene l'esempio precedente chiuda il flusso del file dopo che il file è stato letto, c'è una variazione che è spesso utile.

La variazione è chiamare close() all'interno di un blocco finally. In questo approccio, tutti i metodi che accedono al file sono contenuti all'interno di un blocco try, e il blocco finally è utilizzato per chiudere il file. In questo modo, non importa come termina il blocco try, il file viene chiuso.

Assumendo l'esempio precedente, ecco come il blocco try che legge il file può essere ricodificato:

// 3. A questo punto, il file è aperto e può essere letto.
// Questa parte legge il file byte per byte
// e visualizza ogni byte sulla console.
try {
    do {
        i = finput.read();
        if (i != -1)
            System.out.print((char) i);
    } while (i != -1);
} catch (IOException e) {
    System.out.println("Errore Lettura File");
} finally {
    // 4. Chiude il file
    try {
        finput.close();
    } catch (IOException e) {
        System.out.println("Errore Chiusura File");
    }
}

Sebbene non sia un problema in questo caso, un vantaggio di questo approccio in generale è che se il codice che accede a un file termina a causa di qualche eccezione non correlata all'I/O, il file viene comunque chiuso dal blocco finally.

A volte è più facile racchiudere le porzioni di un programma che aprono il file e accedono al file all'interno di un singolo blocco try (piuttosto che separare i due) e poi utilizzare un blocco finally per chiudere il file. Ad esempio, ecco un altro modo per scrivere il programma MostraFile:

/*
 * Visualizza un file di testo.
 * Per utilizzare questo programma, specificare il nome
 * del file che si desidera vedere.
 * Ad esempio, per vedere un file chiamato TEST.TXT,
 * utilizzare la seguente riga di comando.
 * java MostraFile TEST.TXT
 */

import java.io.*;

class MostraFile {

    public static void main(String[] args) {
        int i;
        FileInputStream finput = null;

        // 1. Conferma che un nome file è stato specificato
        // come argomento della riga di comando.
        if (args.length != 1) {
            System.out.println("Uso: MostraFile nomefile");
            return;
        }

        try {
            // 2. Tenta di aprire il file specificato
            // creando un oggetto FileInputStream.
            // Se il file non può essere aperto,
            // mostra un messaggio di errore
            // e termina il programma.
            finput = new FileInputStream(args[0]);

            // 3. A questo punto, il file è aperto e può essere letto.
            // Questa parte legge il file byte per byte
            // e visualizza ogni byte sulla console.
            do {
                i = finput.read();
                if (i != -1)
                    System.out.print((char) i);
            } while (i != -1);

        } catch (FileNotFoundException e) {
            System.out.println("Impossibile Aprire File");
            return;
        } catch (IOException e) {
            System.out.println("Errore Lettura File");
        } finally {
            // 4. Chiude il file
            try {
                if (finput != null) {
                    finput.close();
                }
            } catch (IOException e) {
                System.out.println("Errore Chiusura File");
            }
        }
    }

}

In questo approccio, si noti che finput è inizializzato a null. Poi, nel blocco finally, il file viene chiuso solo se finput non è null. Questo funziona perché finput sarà non-null solo se il file viene aperto con successo. Quindi, close() non viene chiamato se si verifica un'eccezione durante l'apertura del file.

È possibile rendere la sequenza try/catch nell'esempio precedente un po' più compatta. Poiché FileNotFoundException è una sottoclasse di IOException, non è necessario catturarla separatamente. Ad esempio, ecco la sequenza ricodificata per eliminare la cattura di FileNotFoundException. In questo caso, viene visualizzato il messaggio di eccezione standard, che descrive l'errore.

try {
    // 2. Tenta di aprire il file specificato
    // creando un oggetto FileInputStream.
    // Se il file non può essere aperto,
    // mostra un messaggio di errore
    // e termina il programma.
    finput = new FileInputStream(args[0]);

    // 3. A questo punto, il file è aperto e può essere letto.
    // Questa parte legge il file byte per byte
    // e visualizza ogni byte sulla console.
    do {
        i = finput.read();
        if (i != -1)
            System.out.print((char) i);
    } while (i != -1);

} catch (IOException e) {
    System.out.println("Errore di I/O");
} finally {
    // 4. Chiude il file
    try {
        if (finput != null) {
            finput.close();
        }
    } catch (IOException e) {
        System.out.println("Errore Chiusura File");
    }
}

In questo approccio, qualsiasi errore, incluso un errore nell'apertura del file, viene semplicemente gestito dalla singola istruzione catch.

Si tenga presente, tuttavia, che questo approccio non è appropriato nei casi in cui si desidera gestire separatamente un fallimento nell'apertura di un file, come potrebbe essere causato se un utente digita erroneamente un nome file. In tale situazione, si potrebbe voler richiedere il nome corretto, ad esempio, prima di entrare in un blocco try che accede al file.

Scrittura su un File

Per scrivere su un file, si può utilizzare il metodo write() definito da FileOutputStream. La forma più semplice del metodo write() è mostrata qui:

void write(int valoreBytes) throws IOException

Questo metodo scrive il byte specificato da valoreBytes nel file. Sebbene valoreBytes sia dichiarato come intero, solo gli otto bit di ordine inferiore vengono scritti nel file. Se si verifica un errore durante la scrittura, viene lanciata una IOException. L'esempio successivo utilizza write() per copiare un file:

/*
 * Copia un file.
 * Per utilizzare questo programma, specificare il nome
 * del file sorgente e del file di destinazione.
 * Ad esempio, per copiare un file chiamato PRIMO.TXT
 * in un file chiamato SECONDO.TXT, utilizzare la seguente
 * riga di comando:
 * 
 * java CopiaFile PRIMO.TXT SECONDO.TXT
 */
import java.io.*;

class CopiaFile {

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

        int i;
        FileInputStream finput = null;
        FileOutputStream foutput = null;

        // 1. Verifica che siano stati specificati
        //    due nomi di file come argomenti della riga di comando.
        //    Se non sono stati specificati, mostra un messaggio di errore
        //    e termina il programma.
        if (args.length != 2) {
            System.out.println("Uso: CopiaFile sorgente destinazione");
            return;
        }

        // Copia il file da args[0] a args[1].
        try {

            // 2. Tenta di aprire i file specificati
            //    creando oggetti FileInputStream e FileOutputStream.
            finput = new FileInputStream(args[0]);
            foutput = new FileOutputStream(args[1]);

            // 3. Copia il contenuto del file sorgente in quello di destinazione.
            //    Questa parte legge il file sorgente byte per byte
            //    e scrive ogni byte nel file di destinazione.
            do {
                i = finput.read();
                if (i != -1)
                    foutput.write(i);
            } while (i != -1);

        } catch (IOException e) {
            System.out.println("Errore I/O: " + e);
        } finally {

            // 4. Chiude il file di input
            try {
                if (finput != null)
                    finput.close();
            } catch (IOException e2) {
                System.out.println("Errore Chiusura File Input");
            }

            // 5. Chiude il file di output
            try {
                if (foutput != null)
                    foutput.close();
            } catch (IOException e2) {
                System.out.println("Errore Chiusura File Output");
            }

        }
    }

}

Nel programma, si noti che vengono utilizzati due blocchi try separati quando si chiudono i file. Questo assicura che entrambi i file vengano chiusi, anche se la chiamata a finput.close() lancia un'eccezione.

In generale, si noti che tutti i potenziali errori di I/O sono gestiti nei due programmi precedenti mediante l'uso di eccezioni. Questo differisce da alcuni linguaggi informatici che utilizzano codici di errore per segnalare errori di file.

Ad esempio, in linguaggio C le funzioni di Input e Output, anziché lanciare eccezioni, restituiscono un codice di errore che deve essere controllato dal programmatore. Questo approccio può rendere il codice più difficile da leggere e mantenere, poiché il programmatore deve ricordare di controllare i codici di errore dopo ogni operazione di I/O.

Non solo le eccezioni rendono la gestione dei file più pulita, ma consentono anche a Java di differenziare facilmente la condizione di fine file dagli errori di file quando viene eseguito l'input.