CodeGym /Corsi /JAVA 25 SELF /Elaborazione asincrona dei file di testo

Elaborazione asincrona dei file di testo

JAVA 25 SELF
Livello 56 , Lezione 2
Disponibile

1. Lettura del file a blocchi: ByteBuffer e codifica

Oggi abbiamo raramente a che fare con piccoli file di testo. Di solito si tratta di enormi log dei server, report, file CSV o dump di dati dell'ordine dei gigabyte. Perciò è importante non solo leggere il file, ma farlo in modo efficiente e senza «bloccare» l'applicazione.

L'approccio asincrono aiuta proprio in questo: non blocca il thread principale — che si tratti dell'interfaccia o della logica lato server —, permette di leggere e scrivere grandi quantità di dati in parallelo e rende l'applicazione scalabile quando è necessario lavorare con più file contemporaneamente.

La cosa fondamentale da capire: l'I/O asincrono non rende il disco più veloce — i miracoli non esistono. Permette semplicemente al vostro programma di non annoiarsi in attesa che il disco completi l'operazione e di occuparsi di altro nel frattempo.

Come funziona la lettura asincrona?

Un canale asincrono (AsynchronousFileChannel) legge non stringhe, ma blocchi di byte in un oggetto ByteBuffer. È come trasportare scatole di lettere invece di singole parole. Dopo la lettura dovete trasformare questi byte in stringhe — tenendo conto della codifica!

Esempio: lettura asincrona del file a blocchi

Scriviamo un esempio semplice di lettura asincrona del file a blocchi da 4096 byte e di stampa del contenuto in console.

import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousFileChannel;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.concurrent.Future;
import java.nio.charset.StandardCharsets;
import java.io.IOException;

public class AsyncTextReadExample {
    public static void main(String[] args) throws Exception {
        Path path = Path.of("bigfile.txt");
        try (AsynchronousFileChannel channel = AsynchronousFileChannel.open(path, StandardOpenOption.READ)) {
            ByteBuffer buffer = ByteBuffer.allocate(4096);
            int position = 0;
            Future<Integer> future = channel.read(buffer, position);

            while (future.get() > 0) {
                buffer.flip();
                // Convertiamo i byte in stringa (UTF-8)
                String chunk = StandardCharsets.UTF_8.decode(buffer).toString();
                System.out.print(chunk);
                buffer.clear();
                position += chunk.getBytes(StandardCharsets.UTF_8).length;
                future = channel.read(buffer, position);
            }
        }
    }
}

Punti importanti:

  • Leggiamo il file a parti (per buffer), non per intero.
  • Dopo la lettura i byte vengono decodificati in stringa usando Charset.
  • Non dimenticare buffer.clear() — altrimenti la successiva read non funzionerà!

Perché non basta semplicemente decodificare i byte?

Il problema è che una stringa può «spezzarsi» tra due blocchi, soprattutto se si usa una codifica multibyte (ad esempio, "UTF-8"). Se l'ultimo byte nel buffer è metà di un carattere, il blocco successivo inizierà con il «resto» del carattere. Senza una gestione speciale otterrete «mojibake» o persino un errore di decodifica.

2. Conversione dei byte in stringhe: gestione delle interruzioni

Il problema dell'interruzione delle stringhe

Supponiamo di avere la stringa "Ciao\nMondo\n", e il buffer finisce su "Cia", mentre "o\nMondo\n" cade nel blocco successivo. Se semplicemente concatenate le stringhe, potete perdere caratteri o ottenere una stringa non valida.

Soluzione: usare CharsetDecoder

Java fornisce la classe CharsetDecoder, che sa gestire correttamente tali casi. «Ricorda» i byte non decodificati e ripristina correttamente i caratteri al confine tra blocchi.

Esempio di utilizzo di CharsetDecoder

import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.nio.CharBuffer;
import java.nio.ByteBuffer;

CharsetDecoder decoder = Charset.forName("UTF-8").newDecoder();
ByteBuffer buffer = ... // i vostri byte
CharBuffer charBuffer = CharBuffer.allocate(buffer.capacity());
decoder.decode(buffer, charBuffer, false);
// Ora charBuffer contiene caratteri decodificati correttamente

Nella pratica conserverete il «residuo» tra le letture e decodificherete tenendo conto di tale residuo.

3. Scrittura asincrona di file di testo

La lettura è solo metà del lavoro. Anche la scrittura viene eseguita a blocchi di byte, che prima vanno ottenuti dalle stringhe (codifica).

Esempio: scrittura asincrona di una stringa su file

import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousFileChannel;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.concurrent.Future;
import java.nio.charset.StandardCharsets;

public class AsyncTextWriteExample {
    public static void main(String[] args) throws Exception {
        Path path = Path.of("output.txt");
        String text = "Ciao, mondo!\n";
        ByteBuffer buffer = ByteBuffer.wrap(text.getBytes(StandardCharsets.UTF_8));
        try (AsynchronousFileChannel channel = AsynchronousFileChannel.open(path, StandardOpenOption.WRITE, StandardOpenOption.CREATE)) {
            Future<Integer> future = channel.write(buffer, 0);
            // Per la demo attendiamo il completamento (di solito non serve!)
            future.get();
            System.out.println("Dati scritti in modo asincrono.");
        }
    }
}

Commento: Nei veri scenari asincroni non conviene chiamare future.get() nel thread principale — questo trasforma il codice asincrono in sincrono. Meglio usare CompletionHandler (vedi la lezione precedente).

4. Pratica: lettura asincrona di un grande file di testo e conteggio delle righe

Realizziamo un compito pratico: leggere asincronamente un grande file di testo e contare il numero di righe ("\n"). Risultato: stampare in console il numero di righe.

Esempio con CompletionHandler

import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousFileChannel;
import java.nio.channels.CompletionHandler;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.StandardCharsets;
import java.nio.CharBuffer;
import java.io.IOException;
import java.util.concurrent.atomic.AtomicLong;

public class AsyncLineCounter {
    public static void main(String[] args) throws IOException {
        Path path = Path.of("bigfile.txt");
        AsynchronousFileChannel channel = AsynchronousFileChannel.open(path, StandardOpenOption.READ);

        ByteBuffer buffer = ByteBuffer.allocate(4096);
        AtomicLong position = new AtomicLong(0);
        CharsetDecoder decoder = StandardCharsets.UTF_8.newDecoder();
        StringBuilder leftover = new StringBuilder();
        AtomicLong lines = new AtomicLong(0);

        channel.read(buffer, position.get(), null, new CompletionHandler<Integer, Object>() {
            @Override
            public void completed(Integer result, Object attachment) {
                if (result == -1) {
                    // File letto fino alla fine
                    if (leftover.length() > 0) lines.incrementAndGet();
                    System.out.println("Righe nel file: " + lines.get());
                    try { channel.close(); } catch (IOException e) { e.printStackTrace(); }
                    return;
                }
                buffer.flip();
                CharBuffer charBuffer = CharBuffer.allocate(buffer.remaining());
                decoder.decode(buffer, charBuffer, false);
                charBuffer.flip();
                String chunk = leftover.toString() + charBuffer.toString();
                leftover.setLength(0);

                // Conteggiamo le righe
                int last = 0;
                int idx;
                while ((idx = chunk.indexOf('\n', last)) != -1) {
                    lines.incrementAndGet();
                    last = idx + 1;
                }
                // Residuo (parte della riga dopo l'ultimo \n)
                if (last < chunk.length()) {
                    leftover.append(chunk.substring(last));
                }
                buffer.clear();
                position.addAndGet(result);
                channel.read(buffer, position.get(), null, this);
            }

            @Override
            public void failed(Throwable exc, Object attachment) {
                System.err.println("Errore di lettura: " + exc.getMessage());
                try { channel.close(); } catch (IOException e) { e.printStackTrace(); }
            }
        });

        // Per evitare che il programma termini prima del tempo (solo per l'esempio!)
        try { Thread.sleep(2000); } catch (InterruptedException e) {}
    }
}
  • Usiamo CompletionHandler per codice veramente asincrono.
  • Dopo ogni lettura il buffer viene decodificato con CharsetDecoder.
  • Il residuo della riga non terminata con "\n" viene riportato al blocco successivo.
  • Alla fine del file, se qualcosa è rimasto in leftover, conta anch'esso come riga.
  • Per semplicità l'esempio si «addormenta» per 2000 ms, così l'operazione asincrona può concludersi (nelle applicazioni reali non serve — di solito c'è un ciclo principale o una UI).

5. Scrittura asincrona dei risultati su file

Supponiamo di voler scrivere il risultato (ad esempio, il numero di righe) in un nuovo file — anch'esso in modo asincrono.

import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousFileChannel;
import java.nio.channels.CompletionHandler;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.nio.charset.StandardCharsets;
import java.io.IOException;

public class AsyncWriteResult {
    public static void main(String[] args) throws IOException {
        String result = "Righe nel file: 12345\n";
        ByteBuffer buffer = ByteBuffer.wrap(result.getBytes(StandardCharsets.UTF_8));
        Path path = Path.of("result.txt");

        AsynchronousFileChannel channel = AsynchronousFileChannel.open(
            path, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);

        channel.write(buffer, 0, null, new CompletionHandler<Integer, Object>() {
            @Override
            public void completed(Integer written, Object attachment) {
                System.out.println("Risultato scritto in modo asincrono!");
                try { channel.close(); } catch (IOException e) { e.printStackTrace(); }
            }

            @Override
            public void failed(Throwable exc, Object attachment) {
                System.err.println("Errore di scrittura: " + exc.getMessage());
                try { channel.close(); } catch (IOException e) { e.printStackTrace(); }
            }
        });

        try { Thread.sleep(500); } catch (InterruptedException e) {}
    }
}

6. Consigli per la gestione di dati parziali e delle codifiche

Stringhe parziali tra i blocchi

Se una stringa è divisa tra due blocchi, non cercate di «incollare» i byte manualmente! Usate CharsetDecoder, che gestirà con cura i byte mancanti senza perdere alcun carattere.

Lavorare con diverse codifiche

"UTF-8" — è lo standard per le applicazioni moderne, ma se il file usa un'altra codifica (ad esempio, "Windows-1251" o "UTF-16"), usate il relativo Charset:

import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;

Charset charset = Charset.forName("Windows-1251");
CharsetDecoder decoder = charset.newDecoder();

Uso di CharsetDecoder e CharsetEncoder

Quando leggete o scrivete dati a parti, è importante lavorare correttamente con la codifica. Un carattere può «spezzarsi» tra due blocchi e, senza ulteriore gestione, ne risulterà un pasticcio di byte.

Per evitarlo si usano CharsetDecoder e CharsetEncoder.

In lettura si chiama decode(ByteBuffer, CharBuffer, endOfInput), mentre in scrittura — encode(CharBuffer, ByteBuffer, endOfInput).

Si occupano di far sì che, anche se un carattere è stato diviso tra due blocchi, venga comunque ricomposto e gestito correttamente.

7. Errori tipici nell'elaborazione asincrona dei file di testo

Errore n. 1: Ignorare i residui di riga. Se non si conserva la «coda» della riga tra i blocchi, parte delle righe può andare persa o essere decodificata in modo non corretto.

Errore n. 2: Uso scorretto del buffer. Dimenticare di chiamare buffer.clear() dopo l'elaborazione — la prossima read non funzionerà o i dati saranno non corretti.

Errore n. 3: Codifica non adatta. Se i byte vengono decodificati con un Charset diverso da quello con cui il file è stato scritto, sono possibili «mojibake» o anche errori.

Errore n. 4: Blocco del thread principale. Se chiamate future.get() o Thread.sleep() nel thread della UI, perdete il senso dell'asincronia. Usate CompletionHandler e approcci reattivi.

Errore n. 5: Canale non chiuso al termine. Chiudete sempre il canale (channel.close()) dopo che tutte le operazioni sono terminate, anche in presenza di errore.

1
Compito
JAVA 25 SELF, livello 56, lezione 2
Bloccato
Segretario rapido: conteggio asincrono delle righe in un enorme report 📊
Segretario rapido: conteggio asincrono delle righe in un enorme report 📊
1
Compito
JAVA 25 SELF, livello 56, lezione 2
Bloccato
Fabbrica dati automatizzata: lettura, elaborazione e scrittura asincrona 🤖
Fabbrica dati automatizzata: lettura, elaborazione e scrittura asincrona 🤖
Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION