Les flux bufférisés en Java
Accélérez les E/S Java avec les flux bufférisés — BufferedReader, BufferedWriter, BufferedInputStream, BufferedOutputStream.
Les chapitres sur les flux d'octets et de caractères décrivaient honnêtement les API brutes : chaque appel à FileInputStream.read() ou FileReader.read() est un appel système. Un appel système prend de l'ordre d'une microseconde — rapide en isolation, catastrophique dans une boucle serrée. Lire un fichier de 1 Mo octet par octet représente un million d'appels système ; le même fichier avec un buffer de 8 Ko en représente 128. La différence en temps d'horloge murale est de deux ou trois ordres de grandeur.
Les décorateurs Buffered* se placent entre votre code et le flux brut. Ils maintiennent un byte[] (ou char[]) en mémoire et servent les appels read() depuis ce buffer, en allant vers le système d'exploitation seulement quand le buffer est vide. Côté écriture, ils accumulent les petites écritures dans un buffer et n'effectuent write() vers le système d'exploitation que lorsque le buffer est plein ou que vous appelez flush/close. Même API, coût radicalement différent.
Les quatre classes bufférisées
| Classe | Encapsule |
|---|---|
BufferedInputStream | Un InputStream. Ajoute un buffer byte[] interne. |
BufferedOutputStream | Un OutputStream. Ajoute un buffer byte[] interne. |
BufferedReader | Un Reader. Ajoute un buffer char[] interne et la célèbre méthode readLine(). |
BufferedWriter | Un Writer. Ajoute un buffer char[] interne et une méthode newLine(). |
Ces quatre classes encapsulent tout flux du type correspondant — fichier, socket, pipe, en mémoire — pas seulement les flux de fichiers :
BufferedInputStream in = new BufferedInputStream(new FileInputStream(path.toFile()));
BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream(path.toFile()));
BufferedReader r = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8));
BufferedWriter w = new BufferedWriter(new OutputStreamWriter(out, StandardCharsets.UTF_8));La taille de buffer par défaut est de 8 192 octets/caractères — choisie pour correspondre aux tailles de pages courantes du système d'exploitation. Vous pouvez passer une taille différente au second constructeur, mais la valeur par défaut convient dans pratiquement tous les cas. Des buffers plus grands n'accélèrent pas les choses de façon linéaire ; ils consomment simplement plus de mémoire.
L'API moderne vous fournit ces décorateurs pré-assemblés :
BufferedReader r = Files.newBufferedReader(path); // UTF-8 by default
BufferedWriter w = Files.newBufferedWriter(path, StandardCharsets.UTF_8);
InputStream in = new BufferedInputStream(Files.newInputStream(path));
OutputStream out = new BufferedOutputStream(Files.newOutputStream(path));Files.newBufferedReader / Files.newBufferedWriter encapsulent déjà la classe pont avec le bon jeu de caractères et un BufferedReader/BufferedWriter. Pour le texte, c'est le remplacement en une ligne de la pile manuelle à trois niveaux.
BufferedReader.readLine()
La raison pour laquelle BufferedReader est la classe la plus utilisée dans java.io :
String readLine() throws IOException; // a line, terminator stripped, or null at end
Stream<String> lines(); // Java 8+: line streamreadLine reconnaît \n, \r et \r\n comme terminateurs de ligne et retourne la ligne sans le terminateur. Elle retourne null (pas une chaîne vide, pas -1) à la fin du flux — l'idiome standard de lecture de ligne :
try (BufferedReader r = Files.newBufferedReader(path)) {
String line;
while ((line = r.readLine()) != null) {
process(line);
}
}r.lines() retourne un Stream<String> pour la forme pipeline fonctionnel. Le flux possède le Reader ouvert, donc le bloc try-with-resources autour du lecteur se charge toujours de la fermeture — lines() lui-même n'a pas besoin de sa propre fermeture.
Deux choses à savoir sur readLine(). Premièrement, elle alloue un String par ligne. Pour les boucles de traitement de journaux serrées où l'allocation compte, read(char[]) de bas niveau est ce qu'il vous faut. Deuxièmement, une ligne vide vaut "" (une chaîne vide), pas null — le fichier se termine seulement quand readLine() retourne null.
BufferedWriter.newLine()
La commodité miroir du côté écriture :
void newLine() throws IOException; // platform line separator: \n on Unix, \r\n on WindowsnewLine() écrit ce que la JVM considère comme le séparateur de ligne de la plateforme courante. C'est une fonctionnalité si vous produisez des fichiers pour des yeux humains sur la machine locale ; c'est un bug si vous produisez des fichiers de données, des fichiers journaux, ou quoi que ce soit destiné à une autre machine. Internet fonctionne sur \n. Écrivez toujours \n explicitement quand la sortie doit être portable :
w.write("line one\n"); // portable
w.newLine(); // platform-dependent: \n on Unix, \r\n on WindowsLe même conseil s'applique à PrintWriter.println et au spécificateur de format %n — ils dépendent de la plateforme. Utilisez-les seulement quand la sortie est destinée à une consommation locale.
Le piège du « buffer de queue jamais vidé »
C'est le bug que chaque base de code Java rencontre au moins une fois :
// WRONG
BufferedWriter w = Files.newBufferedWriter(path);
w.write("hello");
return; // 'hello' is sitting in the buffer; nothing on diskUn BufferedWriter ne pousse pas les octets vers le système d'exploitation jusqu'à ce que le buffer soit plein ou que close() s'exécute. Omettez la fermeture et la queue est perdue — Files.size(path) vaut 0 et vous ne savez pas pourquoi. La solution est d'utiliser try-with-resources systématiquement :
try (BufferedWriter w = Files.newBufferedWriter(path)) {
w.write("hello");
} // close() runs here; tail is flushedSi vous avez besoin des données sur disque avant la fermeture — un observateur de queue de journal, ou un autre processus scrutant le fichier — appelez flush() explicitement. Le buffer ne se vide pas automatiquement après chaque écriture ; c'est le prix à payer pour avoir un buffer.
Mark et reset
BufferedReader et BufferedInputStream prennent tous deux en charge une petite API de type « regarder en avant et revenir en arrière » :
in.mark(1024); // remember this position; allow up to 1024 bytes of lookahead
int b = in.read();
in.reset(); // back to the marked positionC'est la seule API de java.io qui vous permet de lire un octet/caractère puis de le remettre en place. C'est le fondement du code qui « inspecte les premiers octets pour déterminer le format » — détection du BOM UTF-8, détection du nombre magique, transfert au parseur. Sans bufférisation, c'est impossible : les flux bruts ne contiennent plus les octets une fois qu'ils ont été lus.
Quand la bufférisation n'aide pas
Deux cas où l'ajout d'un décorateur Buffered* ne sert à rien :
- La source est déjà en mémoire.
ByteArrayInputStreametStringReaderservent déjàread()depuis unbyte[]/Stringen mémoire ; il n'y a pas d'appels système à amortir. - Vous utilisez
Files.readString,Files.readAllBytes,Files.writeoutransferTo. Ces appels effectuent leurs propres E/S bloc par bloc avec un grand buffer interne. Les encapsuler dansBufferedInputStreamest redondant — le JDK bufférisait déjà.
Le cas où la bufférisation aide est le cas initial : vous lisez ou écrivez de petits morceaux (un seul octet, une seule ligne, un appel printf) et la source/destination est un vrai fichier, socket ou pipe.
Un exemple concret : même charge, avec et sans
Le programme ci-dessous copie le même blob de 32 Ko octet par octet d'un fichier temporaire à un autre — une fois avec FileInputStream/FileOutputStream bruts, une fois avec BufferedInputStream/BufferedOutputStream, une fois avec transferTo pour référence. Les affichages de temps d'horloge murale rendent visible le coût de l'absence de buffer. L'exemple lit ensuite les lignes du fichier via un BufferedReader et démontre le piège du « flush oublié » côté écriture.
Ce qu'il faut retenir de l'exécution :
- La copie brute octet par octet était plusieurs ordres de grandeur plus lente que la version bufférisée. Le corps de la boucle était identique ; le seul changement était d'encapsuler les flux de fichiers dans
BufferedInputStream/BufferedOutputStream. C'est la seule raison d'existence de ces décorateurs — même API, beaucoup moins d'appels système. transferToétait aussi rapide que la version bufférisée (voire plus rapide). Pour « copier des octets de A vers B sans transformation »,transferToest ce qu'il vous faut — il bufférise déjà en interne et le JDK a optimisé la boucle. Tournez-vous vers lui avant d'écrire le vôtre.Files.newBufferedReadera retourné unBufferedReaderdirectement. Notez que nous n'avons jamais écritnew BufferedReader(new InputStreamReader(new FileInputStream(...), UTF_8))— c'est la pile à trois niveaux que la fabrique cache.readLine()est sorti de cette pile gratuitement.- L'écrivain « qui fuit » a affiché
0 bytesavantflush(). Ces caractères étaient dans le buffer en mémoire, pas sur disque. Appelerflush()les a poussés vers la sortie ; sans le flush explicite (ou unclose()approprié), ils auraient été perdus. C'est pourquoitry-with-resources autour des écrivains bufférisés n'est pas facultatif — c'est le contrat qui rend l'écriture visible. - La boucle
BufferedReader.readLine()est la forme de traitement de texte la plus courante en Java. Mémorisez la formewhile ((line = r.readLine()) != null): l'affectation dans la condition est idiomatique ici, et le sentinellenull(pas une chaîne vide) est la condition de fin de boucle.
Prochaine étape
La bufférisation résout le coût d'un appel système par appel mais ne change pas ce que les octets signifient. Le chapitre suivant, Java DataInput and DataOutput Streams, couvre les décorateurs qui lisent et écrivent des primitives Java dans un format binaire portable — la couche qui vous permet d'écrire un int dans un fichier et de le relire en tant qu'int sur un autre système d'exploitation.