W3docs

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

ClasseEncapsule
BufferedInputStreamUn InputStream. Ajoute un buffer byte[] interne.
BufferedOutputStreamUn OutputStream. Ajoute un buffer byte[] interne.
BufferedReaderUn Reader. Ajoute un buffer char[] interne et la célèbre méthode readLine().
BufferedWriterUn 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 stream

readLine 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 Windows

newLine() é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 Windows

Le 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 disk

Un 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 flushed

Si 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 position

C'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. ByteArrayInputStream et StringReader servent déjà read() depuis un byte[]/String en mémoire ; il n'y a pas d'appels système à amortir.
  • Vous utilisez Files.readString, Files.readAllBytes, Files.write ou transferTo. Ces appels effectuent leurs propres E/S bloc par bloc avec un grand buffer interne. Les encapsuler dans BufferedInputStream est 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.

java— editable, runs on the server

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 », transferTo est 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.newBufferedReader a retourné un BufferedReader directement. Notez que nous n'avons jamais écrit new 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 bytes avant flush(). Ces caractères étaient dans le buffer en mémoire, pas sur disque. Appeler flush() les a poussés vers la sortie ; sans le flush explicite (ou un close() approprié), ils auraient été perdus. C'est pourquoi try-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 forme while ((line = r.readLine()) != null) : l'affectation dans la condition est idiomatique ici, et le sentinelle null (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.

Pratique

Pratique
Que se passe-t-il avec les données écrites par `w.write('hello')` si vous oubliez de fermer un `BufferedWriter` (et n'appelez jamais `flush()`) ?
Que se passe-t-il avec les données écrites par `w.write('hello')` si vous oubliez de fermer un `BufferedWriter` (et n'appelez jamais `flush()`) ?
Was this page helpful?