CodeGym /Corsi /JAVA 25 SELF /BufferedReader, BufferedWriter: bufferizzazione, vantaggi...

BufferedReader, BufferedWriter: bufferizzazione, vantaggi

JAVA 25 SELF
Livello 36 , Lezione 0
Disponibile

1. Introduzione

Ripassiamo come funzionano FileReader e FileWriter. Ogni volta che chiami i loro metodi read() o write(), avviene un accesso al file system — cioè il computer va davvero sul disco per leggere o scrivere un singolo carattere. Se il file è grande e leggi o scrivi un carattere alla volta, questo si trasforma in una maratona di migliaia o persino milioni di accessi al disco. E il disco, com’è noto, non è l’amico più veloce del programmatore.

Si può immaginare così: devi travasare acqua da un secchio a una bottiglia e lo fai con un cucchiaino. Formalmente funziona, ma è terribilmente lento. Molto più sensato prendere un mestolo o una tazza. Lo stesso vale per i file: leggere o scrivere un carattere alla volta è come trasportare l’acqua con un cucchiaino.

Ecco come avviene:

// Lettura del file carattere per carattere (a 'cucchiaino'!)
try (FileReader reader = new FileReader("big.txt")) {
    int c;
    while ((c = reader.read()) != -1) {
        // elaboriamo il carattere (ad esempio, lo contiamo)
    }
}

Se il file è grande, noterai che il programma procede davvero con estrema lentezza.

Bufferizzazione: che cos’è e a cosa serve

Un buffer è un’area speciale di memoria (di solito un array) in cui i dati vengono caricati o scritti non un carattere alla volta, ma in blocchi più grandi (ad esempio, da 8 KB o più). In parole povere, il buffer è un pezzo di memoria, una sorta di secchio intermedio, in cui i dati vengono letti o scritti in porzioni consistenti, non uno per volta.

Funziona così: in lettura il programma accede una volta al disco, carica nel buffer un intero blocco di dati e poi te li fornisce comodamente un carattere alla volta dalla memoria. Appena il blocco finisce, ne viene caricato un altro. In scrittura — stessa logica: i dati vengono prima accumulati nel buffer e solo dopo inviati in un’unica soluzione al disco (oppure subito, se chiami flush()).

Perché è più veloce? Perché il disco è intrinsecamente lento, soprattutto se lo si «punzecchia» con tante piccole operazioni. Se invece ci vai meno spesso ma prendi di più per volta, tutto risulta molto più rapido. In breve: la bufferizzazione riduce il numero di accessi al disco e accelera il programma.

2. BufferedReader e BufferedWriter: sintassi ed esempi

In Java per bufferizzare la lettura e la scrittura di file di testo si usano due classi:

  • BufferedReader — per la lettura di file di testo.
  • BufferedWriter — per la scrittura di file di testo.

Lavorano sopra i normali Reader/Writer (ad esempio, FileReader/FileWriter), aggiungendo la bufferizzazione.

Lettura del file riga per riga con BufferedReader

Lo scenario più comune è leggere il file per righe. Il metodo readLine() restituisce la riga fino al carattere di nuova riga ("\n" o "\r\n").

import java.io.*;

public class BufferedReaderExample {
    public static void main(String[] args) {
        try (BufferedReader reader = new BufferedReader(new FileReader("input.txt"))) {
            String line;
            while ((line = reader.readLine()) != null) {
                System.out.println(line); // Stampiamo la riga a schermo
            }
        } catch (IOException e) {
            System.out.println("Errore durante la lettura del file: " + e.getMessage());
        }
    }
}

Che cosa succede qui?

Creiamo un FileReader, che sa leggere il file carattere per carattere, e lo avvolgiamo in un BufferedReader. Il lettore bufferizzato prende i dati non un carattere alla volta, ma in blocchi più grandi, li mette in memoria e ce li distribuisce per righe tramite il metodo readLine(). Di conseguenza scrivi semplicemente un ciclo while, ottieni le righe una dopo l’altra e non ti preoccupi delle dimensioni del file: la lettura sarà comunque veloce ed efficiente.

Scrittura del file con BufferedWriter

Scrivere righe su un file può essere altrettanto efficiente:

import java.io.*;

public class BufferedWriterExample {
    public static void main(String[] args) {
        try (BufferedWriter writer = new BufferedWriter(new FileWriter("output.txt"))) {
            writer.write("Hello, world!");
            writer.newLine(); // A capo (dipende dal sistema operativo)
            writer.write("Questa è la seconda riga.");
        } catch (IOException e) {
            System.out.println("Errore durante la scrittura del file: " + e.getMessage());
        }
    }
}

Che cosa succede qui?

Per prima cosa creiamo un FileWriter, poi lo avvolgiamo in un BufferedWriter. Quando chiami write() o newLine(), i dati non vanno subito sul disco. Vengono accumulati in un buffer — una speciale memoria di passaggio. Solo quando il buffer si riempie, oppure chiudi lo stream (o chiami esplicitamente flush()), tutto il testo accumulato viene scritto in un colpo solo nel file. Questo approccio accelera sensibilmente la scrittura e riduce gli accessi al disco.

Come appare in un’unica applicazione?

Supponiamo di scrivere una semplice app-«diario» che salva le voci su file e le stampa a schermo.

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

public class DiaryApp {
    public static void main(String[] args) {
        String fileName = "diary.txt";
        Scanner scanner = new Scanner(System.in);

        // Scrittura di una nuova voce
        System.out.print("Inserisci una nuova voce nel diario: ");
        String entry = scanner.nextLine();

        try (BufferedWriter writer = new BufferedWriter(new FileWriter(fileName, true))) {
            writer.write(entry);
            writer.newLine();
            System.out.println("Voce salvata!");
        } catch (IOException e) {
            System.out.println("Errore durante la scrittura: " + e.getMessage());
        }

        // Lettura di tutte le voci
        System.out.println("\nIl tuo diario:");
        try (BufferedReader reader = new BufferedReader(new FileReader(fileName))) {
            String line;
            while ((line = reader.readLine()) != null) {
                System.out.println(line);
            }
        } catch (IOException e) {
            System.out.println("Errore durante la lettura: " + e.getMessage());
        }
    }
}

Prendiamo il nome del file "diary.txt" e proponiamo all’utente di inserire una nuova voce. Per salvare usiamo FileWriter in modalità append (passiamo true), così le vecchie note non vengono sovrascritte — ogni nuova riga viene aggiunta ordinatamente alla fine del file. L’involucro BufferedWriter rende la scrittura rapida ed efficiente: i dati si accumulano in memoria e poi vanno su disco in un’unica soluzione.

Dopodiché apriamo lo stesso file in lettura tramite BufferedReader. Prende il contenuto in blocchi grandi e te lo fornisce riga per riga. Nel ciclo il programma lo stampa a schermo, e alla fine vedi tutto il tuo «diario» dall’inizio alla fine.

3. Vantaggi di BufferedReader e BufferedWriter

Accelerazione significativa

Quando leggi o scrivi un file con stream bufferizzati, il programma effettua decine, talvolta centinaia di volte meno accessi al disco. Questo è particolarmente evidente con file grandi.

Metodi comodi

  • BufferedReader.readLine() — consente di leggere il file per righe, molto comodo per l’elaborazione di file testuali (ad esempio log, CSV, config).
  • BufferedWriter.newLine() — aggiunge il carattere di nuova riga, inserendo quello corretto in base al sistema operativo.

Semplicità d’uso

  • Le classi si combinano facilmente con altri stream (ad esempio, si può avvolgere InputStreamReader dentro BufferedReader per leggere file con diverse codifiche).
  • Chiudono automaticamente le risorse usando try-with-resources.

Flessibilità

  • Puoi impostare esplicitamente la dimensione del buffer, se quella predefinita (di solito 8 KB) è troppo piccola o troppo grande per il tuo caso:
BufferedReader reader = new BufferedReader(new FileReader("big.txt"), 16384); // buffer da 16 KB

4. Quando usare BufferedReader e BufferedWriter

Usali quando:

  • Lavori con file di testo (log, CSV, grandi quantità di testo).
  • Devi leggere o scrivere il file riga per riga.
  • Serve accelerare il lavoro con file grandi, quando la velocità è importante.
  • Devi elaborare un flusso di dati da rete o da un’altra sorgente che supporta Reader/Writer.

Non usarli:

  • Per file binari (ad esempio immagini, archivi, video) — per questi ci sono InputStream/OutputStream.
  • Se il file è molto piccolo e viene letto/scritto una volta sola per intero — qui il vantaggio della bufferizzazione sarà minimo (ma non farà danni).

5. Dettagli utili

Combinazione con diverse codifiche

Se devi leggere/scrivere file in una specifica codifica (ad esempio "UTF-8", "Windows-1251"), usa InputStreamReader/OutputStreamWriter insieme a stream bufferizzati:

BufferedReader reader = new BufferedReader(
    new InputStreamReader(new FileInputStream("input.txt"), "UTF-8")
);

BufferedWriter writer = new BufferedWriter(
    new OutputStreamWriter(new FileOutputStream("output.txt"), "UTF-8")
);

Svuotamento esplicito del buffer

Talvolta è importante che i dati finiscano sicuramente su disco (ad esempio, se stai scrivendo un log o uno scontrino). In questi casi chiama writer.flush(). Di solito non serve, perché la chiusura dello stream scarica automaticamente il buffer.

Dimensione del buffer

Per impostazione predefinita il buffer è di circa 8 KB. Puoi definire un’altra dimensione se sai che porterà benefici prestazionali (ad esempio, nella gestione di file giganteschi).

Confronto: FileReader/FileWriter vs BufferedReader/BufferedWriter

Classe Velocità su file grandi Comodità della lettura per righe Comodità della scrittura per righe Flessibilità con le codifiche
FileReader/FileWriter Lenta No (solo per carattere) No (solo per carattere) Solo predefinita
BufferedReader/Writer Veloce Sì (readLine()) Sì (newLine()) Sì (tramite InputStreamReader/OutputStreamWriter)

6. Errori tipici con BufferedReader e BufferedWriter

Errore n. 1: hai dimenticato di chiudere lo stream. Se non usi try-with-resources o non chiami close(), il file può rimanere bloccato e i dati potrebbero non essere scritti su disco. Usa sempre try-with-resources!

Errore n. 2: confusione tra file testuali e binari. Tentare di aprire un file binario (".jpg", ".zip") tramite BufferedReader porterà a simboli illeggibili e, con ogni probabilità, a errori. Per i file binari usa InputStream/OutputStream.

Errore n. 3: non usare la bufferizzazione con grandi volumi di dati. Se leggi o scrivi per carattere, il programma sarà lento. Usa sempre la bufferizzazione per i file grandi.

Errore n. 4: non chiamare flush() quando necessario. Se serve che i dati compaiano immediatamente su disco (ad esempio per il logging), chiama writer.flush(). Di solito, però, la chiusura dello stream fa già tutto il necessario.

Errore n. 5: ignorare la codifica. Se apri un file con la codifica sbagliata, il testo può risultare non corretto (caratteri russi che diventano «?», ideogrammi, ecc.). Specifica sempre esplicitamente la codifica necessaria, se è diversa da quella di sistema (ad esempio "UTF-8").

1
Compito
JAVA 25 SELF, livello 36, lezione 0
Bloccato
Estrazione della prima idea dal rotolo antico 📜
Estrazione della prima idea dal rotolo antico 📜
1
Compito
JAVA 25 SELF, livello 36, lezione 0
Bloccato
Registrazione della grande verità negli annali della storia ✍️
Registrazione della grande verità negli annali della storia ✍️
Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION