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
| Clase | Envuelve |
|---|---|
BufferedInputStream | Un InputStream. Añade un buffer byte[] interno. |
BufferedOutputStream | Un OutputStream. Añade un buffer byte[] interno. |
BufferedReader | Un Reader. Añade un buffer char[] interno y el famoso método readLine(). |
BufferedWriter | Un 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 streamreadLine 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 WindowsnewLine() 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 WindowsEl 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 diskUn 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 flushedSi 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 positionEsta 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.
ByteArrayInputStreamyStringReaderya atiendenread()desde unbyte[]/Stringen memoria; no hay llamadas al sistema que amortizar. - Estás usando
Files.readString,Files.readAllBytes,Files.writeotransferTo. Esas llamadas hacen su propia E/S bloque a bloque con un gran buffer interno. Envolverlas enBufferedInputStreames 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.
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. transferTofue tan rápido como la versión buffereada (o más). Para "copiar bytes de A a B sin transformación,"transferToes lo que quieres — ya tiene buffer internamente y el JDK ha ajustado el bucle. Acude a él antes de escribir el tuyo propio.Files.newBufferedReaderdevolvió unBufferedReaderdirectamente. Observa que nunca escribimosnew 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 bytesantes deflush(). Esos caracteres estaban en el buffer en memoria, no en disco. Llamar aflush()los expulsó; sin el flush explícito (o unclose()adecuado), se habrían perdido. Por eso eltry-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 formawhile ((line = r.readLine()) != null): la asignación dentro de la condición es idiomática aquí, y el centinelanull(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.