1. Introduction
Rappelons comment fonctionnent FileReader et FileWriter. Chaque fois que vous appelez la méthode read() ou write(), il y a un accès au système de fichiers — c’est‑à‑dire que l’ordinateur va réellement sur le disque pour lire ou écrire un seul caractère. Si le fichier est volumineux et que vous lisez ou écrivez caractère par caractère, cela se transforme en un marathon de milliers, voire de millions, d’accès au disque. Et le disque, comme on le sait, n’est pas le meilleur ami du développeur.
On peut l’imaginer ainsi : vous devez verser de l’eau d’un seau dans une bouteille et vous le faites avec une cuillère à café. Techniquement, ça marche, mais c’est terriblement long. Il est bien plus raisonnable de prendre une louche ou un gobelet. Pour les fichiers, c’est pareil : lire ou écrire caractère par caractère, c’est comme transporter de l’eau à la cuillère.
Voici comment cela se passe :
// Lecture du fichier caractère par caractère (à la "cuillère" !)
try (FileReader reader = new FileReader("big.txt")) {
int c;
while ((c = reader.read()) != -1) {
// traiter le caractère (par exemple, simplement compter)
}
}
Si le fichier est volumineux, vous remarquerez que le programme est vraiment très lent.
Mise en mémoire tampon : qu’est‑ce que c’est et pourquoi en a‑t‑on besoin
Un tampon est une zone mémoire spéciale (généralement un tableau) dans laquelle les données sont chargées ou écrites non pas caractère par caractère, mais par gros blocs (par exemple 8 Ko ou plus). En termes simples, un tampon est un morceau de mémoire, une sorte de seau intermédiaire, où les données sont lues ou écrites non pas caractère par caractère, mais par grandes portions.
Voici le fonctionnement : lors de la lecture, le programme accède une fois au disque, charge dans le tampon tout un bloc de données, puis les fournit tranquillement caractère par caractère depuis la mémoire. Dès que le bloc est épuisé, le suivant est chargé. Pour l’écriture, c’est la même logique : les données sont d’abord accumulées dans le tampon, puis envoyées d’un seul coup sur le disque (ou immédiatement si vous appelez flush()).
Pourquoi est‑ce plus rapide ? Parce que le disque est lent par nature, surtout si on le sollicite pour des broutilles. En y allant moins souvent mais en prenant plus à chaque fois, tout fonctionne nettement plus vite. En bref : la mise en mémoire tampon réduit le nombre d’accès au disque et accélère l’exécution du programme.
2. BufferedReader et BufferedWriter : syntaxe et exemples
En Java, deux classes sont utilisées pour mettre en mémoire tampon la lecture et l’écriture de fichiers texte :
- BufferedReader — pour lire des fichiers texte.
- BufferedWriter — pour écrire des fichiers texte.
Elles fonctionnent au‑dessus des Reader/Writer classiques (par exemple, FileReader/FileWriter) en ajoutant un tampon.
Lecture d’un fichier ligne par ligne avec BufferedReader
Le scénario le plus courant est de lire le fichier ligne par ligne. La méthode readLine() renvoie la chaîne jusqu’au caractère de fin de ligne ("\n" ou "\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); // Afficher la ligne à l’écran
}
} catch (IOException e) {
System.out.println("Erreur lors de la lecture du fichier : " + e.getMessage());
}
}
}
Que se passe‑t‑il ici ?
Nous créons un FileReader, capable de lire le fichier caractère par caractère, et nous l’enveloppons dans un BufferedReader. Le lecteur tamponné récupère les données par gros blocs, les stocke en mémoire et nous les fournit ligne par ligne grâce à readLine(). Au final, vous écrivez simplement une boucle while, vous obtenez les lignes les unes après les autres sans vous soucier de la taille du fichier : la lecture reste rapide et économe.
Écriture d’un fichier avec BufferedWriter
On peut aussi écrire des lignes dans un fichier efficacement :
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(); // Saut de ligne (dépend de l’OS)
writer.write("C'est la deuxième ligne.");
} catch (IOException e) {
System.out.println("Erreur lors de l'écriture du fichier : " + e.getMessage());
}
}
}
Que se passe‑t‑il ici ?
Nous créons d’abord un FileWriter, puis nous l’enveloppons dans un BufferedWriter. Quand vous appelez write() ou newLine(), les données ne partent pas tout de suite sur le disque. Elles s’accumulent dans un tampon — une couche mémoire intermédiaire. Ce n’est que lorsque le tampon est plein, ou quand vous fermez le flux (ou appelez explicitement flush()), que tout le texte accumulé est écrit d’un coup dans le fichier. Cette approche accélère nettement l’écriture et réduit les accès au disque.
À quoi cela ressemble‑t‑il dans une application complète ?
Supposons que nous écrivons un petit programme « journal » qui enregistre des entrées dans un fichier et les affiche à l’écran.
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);
// Écrire une nouvelle entrée
System.out.print("Saisissez une nouvelle entrée dans le journal : ");
String entry = scanner.nextLine();
try (BufferedWriter writer = new BufferedWriter(new FileWriter(fileName, true))) {
writer.write(entry);
writer.newLine();
System.out.println("Entrée enregistrée !");
} catch (IOException e) {
System.out.println("Erreur lors de l'écriture : " + e.getMessage());
}
// Lire toutes les entrées
System.out.println("\nVotre journal :");
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("Erreur lors de la lecture : " + e.getMessage());
}
}
}
Nous prenons le nom de fichier "diary.txt" et proposons à l’utilisateur de saisir une nouvelle entrée. Pour l’enregistrement, nous utilisons FileWriter en mode append (nous passons true), de sorte que les anciennes notes ne sont pas effacées — chaque nouvelle ligne est ajoutée proprement à la fin du fichier. L’enveloppe BufferedWriter rend l’écriture rapide et économe : les données s’accumulent d’abord en mémoire, puis partent d’un seul coup sur le disque.
Ensuite, nous ouvrons le même fichier en lecture via BufferedReader. Il lit le contenu par gros blocs, et vous restitue les lignes une par une. Dans la boucle, le programme se contente de les afficher, et vous voyez ainsi l’intégralité de votre « journal » du début à la fin.
3. Avantages de BufferedReader et BufferedWriter
Accélération significative
Quand vous lisez ou écrivez un fichier à l’aide de flux tamponnés, le programme effectue des dizaines, voire des centaines de fois moins d’accès au disque. C’est particulièrement visible sur les gros fichiers.
Méthodes pratiques
- BufferedReader.readLine() — permet de lire un fichier ligne par ligne, ce qui est très pratique pour traiter des fichiers texte (par exemple, logs, CSV, configurations).
- BufferedWriter.newLine() — ajoute un saut de ligne en insérant le bon caractère selon l’OS.
Simplicité d’utilisation
- Les classes se combinent facilement avec d’autres flux (par exemple, on peut envelopper un InputStreamReader dans un BufferedReader pour lire des fichiers avec différents encodages).
- Fermeture automatique de toutes les ressources avec try-with-resources.
Flexibilité
- Vous pouvez définir explicitement la taille du tampon si la valeur par défaut (généralement 8 Ko) est trop petite ou trop grande :
BufferedReader reader = new BufferedReader(new FileReader("big.txt"), 16384); // tampon de 16 Ko
4. Quand utiliser BufferedReader et BufferedWriter
À utiliser :
- Si vous travaillez avec des fichiers texte (logs, CSV, gros volumes de texte).
- Quand il faut lire ou écrire un fichier ligne par ligne.
- Pour accélérer le traitement de gros fichiers quand la vitesse est importante.
- Si vous devez traiter un flux de données provenant du réseau ou d’une autre source prenant en charge Reader/Writer.
À ne pas utiliser :
- Pour les fichiers binaires (par exemple, images, archives, vidéos) — utilisez InputStream/OutputStream pour cela.
- Si le fichier est très petit et lu/écrit une seule fois en entier — ici, le gain du tampon sera minimal (mais il ne fera pas de mal non plus).
5. Subtilités utiles
Combinaison avec différents encodages
Si vous devez lire/écrire des fichiers dans un encodage précis (par exemple, "UTF-8", "Windows-1251"), utilisez InputStreamReader/OutputStreamWriter en association avec des flux tamponnés :
BufferedReader reader = new BufferedReader(
new InputStreamReader(new FileInputStream("input.txt"), "UTF-8")
);
BufferedWriter writer = new BufferedWriter(
new OutputStreamWriter(new FileOutputStream("output.txt"), "UTF-8")
);
Vidage explicite du tampon
Parfois, il est important que les données soient réellement écrites sur le disque (par exemple, si vous écrivez un log ou un reçu). Pour cela, appelez writer.flush(). En général, ce n’est pas nécessaire, car la fermeture du flux vide automatiquement le tampon.
Taille du tampon
Par défaut, le tampon fait environ 8 Ko. Vous pouvez définir une autre taille si vous savez que cela apportera un gain de performances (par exemple, pour des fichiers gigantesques).
Comparaison : FileReader/FileWriter vs BufferedReader/BufferedWriter
| Classe | Vitesse sur de gros fichiers | Lecture ligne par ligne aisée | Écriture ligne par ligne aisée | Flexibilité avec les encodages |
|---|---|---|---|---|
| FileReader/FileWriter | Lent | Non (caractère par caractère uniquement) | Non (caractère par caractère uniquement) | Uniquement par défaut |
| BufferedReader/Writer | Rapide | Oui (readLine()) | Oui (newLine()) | Oui (via InputStreamReader/OutputStreamWriter) |
6. Erreurs courantes avec BufferedReader et BufferedWriter
Erreur n° 1 : oubli de fermer le flux. Si vous n’utilisez pas try-with-resources ou n’appelez pas close(), le fichier peut rester verrouillé et les données ne pas être écrites sur le disque. Utilisez toujours try-with-resources !
Erreur n° 2 : confusion entre fichiers texte et binaires. Tenter d’ouvrir un fichier binaire (".jpg", ".zip") via BufferedReader conduira à des caractères illisibles et, très probablement, à des erreurs. Pour les fichiers binaires, utilisez InputStream/OutputStream.
Erreur n° 3 : ne pas utiliser le tampon pour de gros volumes de données. Si vous lisez ou écrivez caractère par caractère, le programme sera lent. Utilisez toujours la mise en mémoire tampon pour les gros fichiers.
Erreur n° 4 : ne pas appeler flush() quand c’est nécessaire. Si vous devez que les données apparaissent immédiatement sur le disque (par exemple, pour la journalisation), appelez writer.flush(). Mais, en général, la fermeture du flux s’en charge.
Erreur n° 5 : ignorer l’encodage. Si vous ouvrez un fichier avec un mauvais encodage, le texte peut s’afficher de manière incorrecte (les lettres deviennent « ? », des idéogrammes, etc.). Indiquez toujours explicitement l’encodage requis s’il diffère de l’encodage système (par exemple "UTF-8").
GO TO FULL VERSION