W3docs

Flujos Buffered en Java

Acelera la E/S en Java con flujos buffered: BufferedReader, BufferedWriter, BufferedInputStream y BufferedOutputStream.

Los capítulos sobre flujos de bytes y caracteres describieron las APIs básicas con honestidad: cada llamada a FileInputStream.read() o FileReader.read() es una llamada al sistema. Una llamada al sistema tarda del orden de un microsegundo — rápida de forma aislada, catastrófica en un bucle cerrado. Leer un archivo de 1 MB byte a byte implica un millón de llamadas al sistema; el mismo archivo con un buffer de 8 KB son 128. La diferencia en tiempo real es de dos o tres órdenes de magnitud.

Los decoradores Buffered* se sitúan entre tu código y el flujo sin procesar. Mantienen un byte[] (o char[]) en memoria y atienden las llamadas read() desde él, acudiendo al SO solo cuando el buffer se vacía. En el lado de escritura, acumulan escrituras pequeñas en un buffer y solo hacen write() al SO cuando el buffer se llena o llamas a flush/close. La misma API, con un coste completamente diferente.

Las cuatro clases buffered

ClaseEnvuelve
BufferedInputStreamUn InputStream. Añade un buffer byte[] interno.
BufferedOutputStreamUn OutputStream. Añade un buffer byte[] interno.
BufferedReaderUn Reader. Añade un buffer char[] interno y el famoso método readLine().
BufferedWriterUn Writer. Añade un buffer char[] interno y un método newLine().

Las cuatro envuelven cualquier flujo del tipo correspondiente — archivo, socket, tubería, en memoria — no solo flujos de archivo:

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));

El tamaño de buffer por defecto es 8192 bytes/chars — elegido para coincidir con los tamaños de página habituales del SO. Puedes pasar un tamaño diferente al segundo constructor, pero el valor por defecto funciona bien en prácticamente todos los casos. Los buffers más grandes no aceleran las cosas de forma lineal; solo consumen más memoria.

La API moderna te proporciona estos decoradores ya ensamblados:

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 ya envuelven la clase puente con el charset correcto y un BufferedReader/BufferedWriter. Para texto, eso es el reemplazo en una línea de la pila manual de tres niveles.

BufferedReader.readLine()

La razón por la que BufferedReader es la clase más usada en java.io:

String readLine() throws IOException;          // a line, terminator stripped, or null at end
Stream<String> lines();                         // Java 8+: line stream

readLine reconoce \n, \r y \r\n como terminadores de línea y devuelve la línea sin el terminador. Devuelve null (no una cadena vacía, no -1) al final del flujo — el idioma estándar para leer líneas:

try (BufferedReader r = Files.newBufferedReader(path)) {
  String line;
  while ((line = r.readLine()) != null) {
    process(line);
  }
}

r.lines() devuelve un Stream<String> para la forma de pipeline funcional. El flujo es propietario del Reader abierto, por lo que el bloque try-with-resources alrededor del reader sigue haciendo el trabajo de cierre — lines() en sí no necesita su propio close.

Dos cosas a tener en cuenta sobre readLine(). Primero, asigna un String por línea. Para bucles de procesamiento de registros ajustados donde la asignación importa, el read(char[]) de bajo nivel es lo que quieres. Segundo, una línea vacía es "" (una cadena vacía), no null — el archivo termina solo cuando readLine() devuelve null.

BufferedWriter.newLine()

La conveniencia espejo en el lado de escritura:

void newLine() throws IOException;             // platform line separator: \n on Unix, \r\n on Windows

newLine() escribe lo que la JVM considera el separador de línea de la plataforma actual. Eso es una característica si estás produciendo archivos para ojos humanos en la máquina local; es un error si estás produciendo archivos de datos, archivos de registro o cualquier cosa destinada a otra máquina. Internet funciona con \n. Siempre escribe \n explícitamente cuando la salida necesita ser portable:

w.write("line one\n");                          // portable
w.newLine();                                    // platform-dependent: \n on Unix, \r\n on Windows

El mismo consejo vale para PrintWriter.println y el especificador de formato %n — son dependientes de la plataforma. Úsalos solo cuando la salida sea para consumo local.

La trampa del "buffer de cola nunca vaciado"

Este es el error que todo código base Java encuentra al menos una vez:

// WRONG
BufferedWriter w = Files.newBufferedWriter(path);
w.write("hello");
return;                                          // 'hello' is sitting in the buffer; nothing on disk

Un BufferedWriter no envía bytes al SO hasta que el buffer se llena o se ejecuta close(). Omite el close y el final se pierde — Files.size(path) es 0 y no tienes ni idea de por qué. La solución es try-with-resources en todo momento:

try (BufferedWriter w = Files.newBufferedWriter(path)) {
  w.write("hello");
}                                                // close() runs here; tail is flushed

Si necesitas los datos en disco antes del close — un observador de la cola del registro, u otro proceso que sondea el archivo — llama a flush() explícitamente. El buffer no se vacía automáticamente después de cada escritura; ese es el precio de tener un buffer.

Mark y reset

BufferedReader y BufferedInputStream admiten una pequeña API de "anticipar y rebobinar":

in.mark(1024);                                   // remember this position; allow up to 1024 bytes of lookahead
int b = in.read();
in.reset();                                      // back to the marked position

Esta es la única API de java.io que te permite leer un byte/char y luego devolverlo. Es la base del código de "echar un vistazo a los primeros bytes para averiguar el formato" — detección de BOM UTF-8, verificación de números mágicos, traspaso a analizadores. Sin buffering no puedes hacerlo: los flujos en bruto ya no tienen los bytes una vez que han sido leídos.

Cuándo el buffering no ayuda

Dos casos en los que añadir un decorador Buffered* no aporta nada:

  • La fuente ya está en memoria. ByteArrayInputStream y StringReader ya atienden read() desde un byte[]/String en memoria; no hay llamadas al sistema que amortizar.
  • Estás usando Files.readString, Files.readAllBytes, Files.write o transferTo. Esas llamadas hacen su propia E/S bloque a bloque con un gran buffer interno. Envolverlas en BufferedInputStream es redundante — el JDK ya lo ha buffereado.

El caso en que el buffering ayuda es el original: estás leyendo o escribiendo en fragmentos pequeños (un solo byte, una sola línea, una llamada printf) y la fuente/destino es un archivo real, socket o tubería.

Un ejemplo práctico: la misma carga, con y sin buffering

El programa a continuación copia el mismo blob de 32 KB byte a byte desde un archivo temporal a otro — una vez con FileInputStream/FileOutputStream en bruto, una vez con BufferedInputStream/BufferedOutputStream, y una vez con transferTo como referencia. Las impresiones de tiempo en pared hacen visible el coste del buffer faltante. El ejemplo luego lee las líneas del archivo a través de un BufferedReader y demuestra la trampa del "olvidó hacer flush" en el lado de escritura.

java— editable, runs on the server

Lo que extraer de la ejecución:

  • La copia byte a byte en bruto fue órdenes de magnitud más lenta que la buffereada. El cuerpo del bucle era idéntico; el único cambio fue envolver los flujos de archivo en BufferedInputStream/BufferedOutputStream. Esa es la razón completa por la que existen estos decoradores — la misma API, muchísimas menos llamadas al sistema.
  • transferTo fue tan rápido como la versión buffereada (o más). Para "copiar bytes de A a B sin transformación," transferTo es lo que quieres — ya tiene buffer internamente y el JDK ha ajustado el bucle. Acude a él antes de escribir el tuyo propio.
  • Files.newBufferedReader devolvió un BufferedReader directamente. Observa que nunca escribimos new BufferedReader(new InputStreamReader(new FileInputStream(...), UTF_8)) — esa pila de tres niveles es lo que oculta la factoría. readLine() salió de esa pila de forma gratuita.
  • El writer con pérdidas imprimió 0 bytes antes de flush(). Esos caracteres estaban en el buffer en memoria, no en disco. Llamar a flush() los expulsó; sin el flush explícito (o un close() adecuado), se habrían perdido. Por eso el try-with-resources alrededor de los writers buffereados no es opcional — es el contrato que hace visible la escritura.
  • El bucle BufferedReader.readLine() es la forma de procesamiento de texto más común en Java. Memoriza la forma while ((line = r.readLine()) != null): la asignación dentro de la condición es idiomática aquí, y el centinela null (no una cadena vacía) es la condición de fin del bucle.

Qué viene a continuación

El buffering resuelve el coste de llamada al sistema por llamada, pero no cambia lo que significan los bytes. El siguiente capítulo, Java DataInput y DataOutput Streams, cubre los decoradores que leen y escriben primitivos de Java en un formato binario portable — la capa que te permite escribir un int en un archivo y leerlo de vuelta como un int en un SO diferente.

Práctica

Práctica
¿Qué pasa con los datos escritos por `w.write('hello')` si olvidas cerrar un `BufferedWriter` (y nunca llamas a `flush()`)?
¿Qué pasa con los datos escritos por `w.write('hello')` si olvidas cerrar un `BufferedWriter` (y nunca llamas a `flush()`)?
Was this page helpful?