CodeGym /Cours /JAVA 25 SELF /Traitement asynchrone des fichiers texte

Traitement asynchrone des fichiers texte

JAVA 25 SELF
Niveau 56 , Leçon 2
Disponible

1. Lecture de fichier par blocs : ByteBuffer et encodage

De nos jours, nous traitons rarement de petits fichiers texte. Le plus souvent, il s’agit d’énormes journaux de serveurs, de rapports, de fichiers CSV ou de dumps de données de plusieurs gigaoctets. Il est donc important non seulement de lire le fichier, mais de le faire efficacement et sans « gel » de l’application.

L’approche asynchrone aide précisément à cela : elle ne bloque pas le thread principal — qu’il s’agisse de l’interface ou de la logique serveur —, permet de lire et d’écrire de gros volumes de données en parallèle et rend l’application évolutive lorsqu’il faut travailler avec plusieurs fichiers à la fois.

L’essentiel à comprendre : l’E/S asynchrone n’accélère pas le disque lui‑même — pas de miracle. Elle permet simplement à votre programme de ne pas s’ennuyer en attendant que le disque termine l’opération et de s’occuper d’autres tâches pendant ce temps.

Comment fonctionne la lecture asynchrone ?

Un canal asynchrone (AsynchronousFileChannel) ne lit pas des lignes, mais des blocs d’octets dans un objet ByteBuffer. C’est comme transporter des caisses de lettres plutôt que des mots isolés. Après la lecture, vous devez transformer ces octets en chaînes — en tenant compte de l’encodage !

Exemple : lecture asynchrone du fichier par blocs

Écrivons un petit exemple de lecture asynchrone d’un fichier par blocs de 4096 octets et d’affichage du contenu dans la 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();
                // Convertir les octets en chaîne (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);
            }
        }
    }
}

Points importants :

  • Nous lisons le fichier par blocs (tampon), pas en entier.
  • Après la lecture, les octets sont décodés en chaîne à l’aide de Charset.
  • N’oubliez pas buffer.clear() — sinon la lecture suivante ne fonctionnera pas !

Pourquoi il ne suffit pas de décoder les octets ?

Le problème, c’est qu’un caractère peut être « coupé » entre deux blocs, surtout si l’on utilise un encodage multioctet (par exemple, "UTF-8"). Si le dernier octet du tampon n’est que la moitié d’un caractère, le bloc suivant commencera par le « reste » de ce caractère. Sans traitement spécial, vous obtiendrez des caractères illisibles, voire une erreur de décodage.

2. Conversion des octets en chaînes : gestion des coupures

Problème de coupure de lignes

Supposons que vous ayez la chaîne "Bonjour\nMonde\n", et que le tampon se termine sur "Bonj", tandis que "our\nMonde\n" arrive dans le bloc suivant. Si vous concaténez naïvement les chaînes, vous risquez de perdre des caractères ou d’obtenir une chaîne incorrecte.

Solution : utiliser CharsetDecoder

Java fournit la classe CharsetDecoder, qui sait gérer correctement ces cas. Il « mémorise » les octets non décodés et reconstitue correctement les caractères à la jonction des blocs.

Exemple d’utilisation de 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 = ... // vos octets
CharBuffer charBuffer = CharBuffer.allocate(buffer.capacity());
decoder.decode(buffer, charBuffer, false);
// Maintenant, charBuffer contient des caractères correctement décodés

Dans un cas réel, vous conserverez un « reste » entre les lectures et décoderez en tenant compte de ce reste.

3. Écriture asynchrone des fichiers texte

La lecture n’est que la moitié du travail. L’écriture s’effectue également par blocs d’octets qu’il faut d’abord obtenir à partir des chaînes (encodage).

Exemple : écriture asynchrone d’une chaîne dans un fichier

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 = "Bonjour, monde!\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);
            // Pour la démonstration, attendons la fin (normalement, il ne faut pas le faire !)
            future.get();
            System.out.println("Les données ont été écrites de manière asynchrone.");
        }
    }
}

Commentaire : Dans des scénarios réellement asynchrones, il ne faut pas appeler future.get() dans le thread principal — cela rend le code asynchrone synchrone. Il vaut mieux utiliser un CompletionHandler (voir la leçon précédente).

4. Pratique : lecture asynchrone d’un grand fichier texte et comptage des lignes

Implémentons une tâche pratique : lire un grand fichier texte de façon asynchrone et compter le nombre de lignes ("\n"). Résultat : afficher le nombre de lignes dans la console.

Exemple avec 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) {
                    // Fichier entièrement lu
                    if (leftover.length() > 0) lines.incrementAndGet();
                    System.out.println("Lignes dans le fichier : " + 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);

                // Compter les lignes
                int last = 0;
                int idx;
                while ((idx = chunk.indexOf('\n', last)) != -1) {
                    lines.incrementAndGet();
                    last = idx + 1;
                }
                // Reste (partie de la ligne après le dernier \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("Erreur de lecture : " + exc.getMessage());
                try { channel.close(); } catch (IOException e) { e.printStackTrace(); }
            }
        });

        // Pour éviter que le programme ne se termine trop tôt (à des fins d’exemple uniquement !)
        try { Thread.sleep(2000); } catch (InterruptedException e) {}
    }
}
  • Nous utilisons CompletionHandler pour du code réellement asynchrone.
  • Après chaque lecture, le tampon est décodé avec CharsetDecoder.
  • Le reste d’une ligne qui ne se termine pas par "\n" est reporté au bloc suivant.
  • À la fin du fichier, s’il reste quelque chose dans leftover, cela compte aussi comme une ligne.
  • Pour la simplicité, l’exemple « dort » pendant 2000 ms afin de laisser l’opération asynchrone se terminer (inutile dans les applications réelles — il y a généralement une boucle principale ou une UI).

5. Écriture asynchrone des résultats dans un fichier

Supposons que nous voulions écrire le résultat (par exemple, le nombre de lignes) dans un nouveau fichier — également de manière asynchrone.

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 = "Lignes dans le fichier : 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("Le résultat a été écrit de manière asynchrone !");
                try { channel.close(); } catch (IOException e) { e.printStackTrace(); }
            }

            @Override
            public void failed(Throwable exc, Object attachment) {
                System.err.println("Erreur d’écriture : " + exc.getMessage());
                try { channel.close(); } catch (IOException e) { e.printStackTrace(); }
            }
        });

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

6. Conseils pour la gestion des données partielles et des encodages

Lignes partielles entre les blocs

Si une ligne est scindée entre deux blocs, n’essayez pas de « recoller » les octets à la main ! Utilisez CharsetDecoder, qui gérera proprement les octets manquants et ne perdra aucun caractère.

Travailler avec différents encodages

"UTF-8" est la norme pour les applications modernes, mais si le fichier utilise un autre encodage (par exemple, "Windows-1251" ou "UTF-16"), utilisez le Charset approprié :

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

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

Utilisation de CharsetDecoder et CharsetEncoder

Lorsque vous lisez ou écrivez des données par morceaux, il est important de manipuler correctement l’encodage. Un caractère peut être « coupé » entre deux blocs, et sans traitement supplémentaire vous obtiendrez une bouillie d’octets.

Pour éviter cela, on utilise CharsetDecoder et CharsetEncoder.

À la lecture, on appelle decode(ByteBuffer, CharBuffer, endOfInput), et à l’écriture — encode(CharBuffer, ByteBuffer, endOfInput).

Ils veillent à ce que, même si un caractère se trouve séparé entre deux blocs, il soit tout de même reconstitué et traité correctement.

7. Erreurs courantes lors du traitement asynchrone des fichiers texte

Erreur n° 1 : ignorer le reste de la ligne. Si vous ne conservez pas la « fin » de la ligne entre les blocs, certaines lignes peuvent être perdues ou mal décodées.

Erreur n° 2 : mauvaise gestion du tampon. Vous avez oublié d’appeler buffer.clear() après le traitement — la lecture suivante peut échouer ou les données être incorrectes.

Erreur n° 3 : utiliser un encodage inadapté. Si les octets sont décodés avec un Charset différent de celui utilisé lors de l’écriture du fichier, vous risquez d’obtenir des caractères illisibles, voire des erreurs.

Erreur n° 4 : blocage du thread principal. Si vous appelez future.get() ou Thread.sleep() dans le thread UI, vous perdez l’intérêt de l’asynchronisme. Utilisez CompletionHandler et des approches réactives.

Erreur n° 5 : canal non fermé après la fin. Fermez toujours le canal (channel.close()) une fois toutes les opérations terminées, même en cas d’erreur.

Commentaires
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION