Lezione 10 · Canale 1 · giovedì 15 ottobre 2026

Lettura e scrittura di file con gli stream in Java

Progettazione del Software

Stream e filtri

Un programma legge e scrive dati attraverso gli stream (flussi): da uno stream di input si legge, su uno stream di output si scrive.

Gli stream si compongono. Un filtro riceve i dati da un altro stream e li offre in una forma più comoda, per esempio per righe o per interi, oppure li trasforma, per esempio decomprimendo un file ZIP o decifrando un file crittato. Si parte dallo stream collegato al file e gli si sovrappongono i filtri che servono, in cascata. Lo stesso meccanismo vale per le connessioni di rete, trattate come file da cui leggere e su cui scrivere.

Per i caratteri le classi astratte sono Reader e Writer, per i byte InputStream e OutputStream. Il metodo read() di un Reader restituisce un intero: il carattere letto, oppure -1 quando i dati sono finiti.

I lettori di caratteri

Readerread(): interoBufferedReaderreadLine(): stringaInputStreamReaderFileReader
FileReader eredita read da InputStreamReader, che è un Reader. Anche BufferedReader è un Reader, e aggiunge readLine.

BufferedReader riceve un Reader nel costruttore ed è a sua volta un Reader: per questo i filtri si possono incastrare uno sopra l'altro.

Scrivere un file

FileWriter scrive caratteri su un file. new FileWriter("prova.txt") crea il file nella cartella del progetto, e se esiste già lo sostituisce.

import java.io.FileWriter;
import java.io.IOException;

public class Scrittura {
    public static void main(String[] args)
            throws IOException {
        FileWriter fw = new FileWriter("prova.txt");
        fw.write("prima linea\n");
        fw.write("seconda linea\n");
        fw.flush();
        fw.close();
    }
}

Nei frammenti seguenti si omettono gli import e il main, che dichiara throws IOException.

  • Eccezioni. L'apertura può fallire: la cartella può non essere accessibile, possono mancare i permessi, in lettura il file può non esistere. In C una fopen fallita restituisce un valore di errore che il programma deve controllare. In Java il compilatore impone di scegliere: gestire l'eccezione, oppure dichiarare throws IOException su main, e allora l'errore termina il programma. IOException è l'eccezione generale delle operazioni di input/output. Le eccezioni sono oggetti e formano una gerarchia.
  • Terminatore di riga. write non va a capo: il \n si scrive nei dati, altrimenti le due stringhe finiscono sulla stessa riga. Conta anche nei protocolli di rete, dove un server può aspettarsi un terminatore preciso, che cambia da un sistema o da un protocollo all'altro.
  • Buffer. write non scrive subito: accumula i caratteri in un buffer in memoria e li emette quando vuole. flush() forza l'emissione. È facile dimenticarlo, soprattutto in rete: senza flush() i dati restano nel computer e non vengono spediti. close() chiude il file e svuota a sua volta il buffer.

Leggere un file, per carattere e per riga

FileReader apre un file di testo in lettura. Il suo read() restituisce un intero, che per diventare un carattere richiede un cast:

FileReader fr = new FileReader("prova.txt");
int c = fr.read();
System.out.println((char) c);
fr.close();

Leggere un carattere per volta è scomodo. Si passa allora il FileReader al costruttore di un BufferedReader, che ne conserva il riferimento. Quando si chiama readLine(), il BufferedReader chiede caratteri al FileReader finché non completa una riga, poi la restituisce. Finite le righe, restituisce null.

Il ciclo di lettura ha sempre la stessa forma: si chiede una riga, se non c'è si esce, altrimenti la si elabora e si ricomincia.

FileReader fr = new FileReader("prova.txt");
BufferedReader br = new BufferedReader(fr);
while (true) {
    String s = br.readLine();
    if (s == null)
        break;
    System.out.println(s);
}
br.close();

Esercizio: sommare gli interi di un file

Il file interi.txt contiene i valori 4, 3, 6, 2, 9, uno per riga. Il ciclo è quello di prima: cambia solo l'elaborazione. readLine dà una stringa, che si converte in intero e si somma.

BufferedReader br =
        new BufferedReader(new FileReader("interi.txt"));
int somma = 0;
while (true) {
    String s = br.readLine();
    if (s == null)
        break;
    somma += Integer.parseInt(s);
}
br.close();
System.out.println(somma);

La somma è 4+3+6+2+9=244+3+6+2+9=24.

Se più interi stanno sulla stessa riga, Integer.parseInt lancia NumberFormatException: vuole una stringa che contenga un solo intero. Serve un filtro che legga gli interi uno alla volta.

Lo Scanner

Lo Scanner riceve il FileReader al posto del BufferedReader, quindi la struttura è la stessa: un oggetto che chiede i caratteri al FileReader. La differenza è nei metodi di lettura, perché lo Scanner non è limitato alle righe:

MetodoLegge
nextInt()un intero
nextLine()una riga
next()una parola

I metodi hasNextInt() e hasNext() dicono se c'è ancora un intero, o una parola. Il sommatore si riduce a un solo ciclo, senza conversioni:

Scanner sc = new Scanner(new FileReader("interi.txt"));
int somma = 0;
while (sc.hasNextInt())
    somma += sc.nextInt();
sc.close();
System.out.println(somma);

Il risultato è ancora 24. Aggiungere un 10 lo porta a 34, in qualunque riga lo si metta, perché lo Scanner legge intero per intero.

Le letture si combinano secondo la struttura del file. Per un file con una riga di testo, poi interi, poi parole:

Scanner sc = new Scanner(new FileReader("misto.txt"));
String titolo = sc.nextLine();
int somma = 0;
while (sc.hasNextInt())
    somma += sc.nextInt();
while (sc.hasNext())
    System.out.println(sc.next());
sc.close();

Il primo ciclo termina al primo token che non è un intero, e il secondo legge da lì le parole una alla volta.

Per default le parole sono separate da spazi. Con sc.useDelimiter(",") le separa solo la virgola: spazi e ritorni a capo restano dentro il token. L'argomento è un pattern, non un singolo carattere, ma per un delimitatore semplice basta il carattere.

Dati CSV e costruttore da una stringa

In un file CSV (comma-separated values) ogni riga contiene i dati di un record, separati da virgole. Qui ogni riga descrive una persona, nell'ordine dei parametri del costruttore: nome,cognome,nascita.

La riga si legge con un BufferedReader. Per separarne i campi si crea uno Scanner a partire dalla stringa: lo Scanner accetta come sorgente anche una String, e allora chiede i caratteri a quella, non a un FileReader.

In Persona si aggiunge un secondo costruttore, che riceve la riga:

public class Persona {
    private String nome, cognome;
    private int nascita;

    public Persona(String nome, String cognome,
            int nascita) {
        this.nome = nome;
        this.cognome = cognome;
        this.nascita = nascita;
    }

    public Persona(String riga) {
        Scanner sc = new Scanner(riga);
        sc.useDelimiter(",");
        this.nome = sc.next();
        this.cognome = sc.next();
        this.nascita = sc.nextInt();
        sc.close();
    }

    public String toString() {
        return nome + " " + cognome + " (" + nascita + ")";
    }
}

Due costruttori possono avere lo stesso nome, quello della classe, se i parametri sono diversi: Java sceglie in base agli argomenti. Il costruttore con una stringa fa l'opposto di toString: da una stringa produce una persona, mentre toString da una persona produce una stringa.

Il ciclo di lettura costruisce le persone e le raccoglie in una lista:

BufferedReader br =
        new BufferedReader(new FileReader("dati.csv"));
LinkedList<Persona> lista = new LinkedList<Persona>();
while (true) {
    String riga = br.readLine();
    if (riga == null)
        break;
    lista.add(new Persona(riga));
}
br.close();
System.out.println(lista);

La stampa della lista usa il toString di Persona per ogni elemento: li separa con virgole e li racchiude fra parentesi quadre. I caratteri vengono dal FileReader, il BufferedReader li raccoglie in righe e lo Scanner le divide nei campi.

Leggere un archivio ZIP

Un file ZIP contiene byte compressi. ZipInputStream, che li decomprime, non accetta un FileReader: vuole uno stream di byte, e si parte da FileInputStream, che ha praticamente lo stesso read. Un archivio può contenere più file, quindi prima si sceglie quale leggere: getNextEntry() si posiziona sul file successivo, e la prima volta sul primo. Lo stream si passa poi a uno Scanner, che accetta qualunque InputStream.

FileInputStream fs = new FileInputStream("prova.zip");
ZipInputStream zs = new ZipInputStream(fs);
zs.getNextEntry();
Scanner sc = new Scanner(zs);
for (int i = 0; i < 3; i++)
    System.out.println(sc.nextLine());
sc.close();

Il file contiene esattamente tre righe, quindi bastano tre nextLine().

Catena per leggere un file ZIP

sc : Scannerzs : ZipInputStreamfs : FileInputStreamlegge dalegge da
Ogni oggetto chiede i dati al successivo: fs legge i byte del file, zs li decomprime, sc ne ricava righe e parole.

Lo stesso schema serve per un file crittato: fra il file e lo Scanner si mette un filtro che decifra.

Un input stream scritto a mano

Un proprio input stream si ottiene estendendo InputStream e implementando read(): restituisce il prossimo byte, un valore da 0 a 255, oppure -1 quando i dati sono finiti. Si passa poi a uno Scanner come ogni altro stream.

Nell'esempio il read() di MioInputStream sceglie a caso cosa restituire: di solito il carattere A, qualche volta uno spazio, molto raramente -1. Lo Scanner lo usa come un file:

Scanner sc = new Scanner(new MioInputStream());
while (sc.hasNext())
    System.out.println(sc.next());
sc.close();

Lo Scanner chiede i caratteri a MioInputStream invece che a un file e con essi compone le parole: stampa sequenze di A di lunghezza diversa, ogni volta diverse, e a un certo punto termina.

La composizione vale anche in uscita: un filtro fra il programma e il file può comprimere o cifrare i dati prima di scriverli.

Formulario

Scrittura

  • new FileWriter("prova.txt")
  • write(s): non va a capo
  • flush(): emette il buffer
  • close(): chiude il file
  • throws IOException su main

Lettura

  • read(): carattere o -1
  • readLine(): riga o null
  • new BufferedReader(fr)

Scanner

  • new Scanner(sorgente)
  • da lettore, stream o stringa
  • nextInt(): un intero
  • nextLine(): una riga
  • next(): una parola
  • hasNextInt(), hasNext()
  • useDelimiter(",")

File ZIP

  • FileInputStream
  • ZipInputStream
  • getNextEntry()

Stream proprio

  • extends InputStream
  • read(): byte o -1