CodeGym /Cursos /JAVA 25 SELF /BufferedReader, BufferedWriter: almacenamiento en búfer, ...

BufferedReader, BufferedWriter: almacenamiento en búfer, ventajas

JAVA 25 SELF
Nivel 36, Lección 0
Disponible

1. Introducción

Recordemos cómo funcionan FileReader y FileWriter. Cada vez que llamas a su método read() o write(), se accede al sistema de archivos — es decir, el ordenador va realmente al disco para leer o escribir un solo carácter. Si el archivo es grande y lees o escribes un carácter cada vez, esto se convierte en una maratón de miles o incluso millones de accesos al disco. Y el disco, como es sabido, no es el amigo más rápido del programador.

Puedes imaginarlo así: necesitas trasvasar agua de un cubo a una botella y lo haces con una cucharilla. En teoría funciona, pero es terriblemente lento. Mucho más sensato es usar un cazo o una taza. Lo mismo con los archivos: leer o escribir carácter a carácter es como transportar agua con una cucharilla.

Así es como ocurre:

// Lectura de un archivo carácter a carácter (¡a "cucharilla"!)
try (FileReader reader = new FileReader("big.txt")) {
    int c;
    while ((c = reader.read()) != -1) {
        // procesamos el carácter (por ejemplo, simplemente contamos)
    }
}

Si el archivo es grande, notarás que el programa funciona muy, muy despacio.

Almacenamiento en búfer: qué es y para qué sirve

Un búfer es una zona especial de memoria (normalmente, un array) donde los datos se cargan o se escriben no carácter a carácter, sino en bloques grandes (por ejemplo, de 8 KB o más). Dicho de forma sencilla, un búfer es un trozo de memoria, algo así como un cubo intermedio, donde los datos se leen o escriben no carácter a carácter, sino en porciones grandes.

Funciona así: al leer, el programa accede una vez al disco, carga en el búfer un bloque completo de datos y después te los va entregando cómodamente carácter a carácter desde la memoria. En cuanto se agota el bloque, se carga el siguiente. Al escribir sucede lo mismo: los datos primero se acumulan en el búfer y luego se envían al disco de una sola vez (o inmediatamente si llamas a flush()).

¿Por qué es más rápido? Porque el disco de por sí es lento, sobre todo si lo haces trabajar “a pellizcos”. En cambio, si acudes a él con menos frecuencia pero tomas más cantidad de una sola vez, todo va notablemente más ágil. En pocas palabras: el almacenamiento en búfer reduce la cantidad de accesos al disco y acelera el programa.

2. BufferedReader y BufferedWriter: sintaxis y ejemplos

En Java, para almacenar en búfer la lectura y escritura de archivos de texto se usan dos clases:

  • BufferedReader — para leer archivos de texto.
  • BufferedWriter — para escribir archivos de texto.

Funcionan sobre los Reader/Writer habituales (por ejemplo, FileReader/FileWriter), añadiendo almacenamiento en búfer.

Leer un archivo por líneas con BufferedReader

El caso más común es leer un archivo línea a línea. El método readLine() devuelve la cadena hasta el carácter de nueva línea ("\n" o "\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); // Mostramos la línea en pantalla
            }
        } catch (IOException e) {
            System.out.println("Error al leer el archivo: " + e.getMessage());
        }
    }
}

¿Qué ocurre aquí?

Creamos un FileReader, que sabe leer el archivo carácter a carácter, y lo envolvemos en un BufferedReader. El lector con búfer toma los datos en porciones grandes, los guarda en memoria y nos los entrega por líneas con el método readLine(). En consecuencia, simplemente escribes un bucle while, obtienes líneas una tras otra y no te preocupas por el tamaño del archivo: la lectura seguirá siendo rápida y eficiente.

Escribir un archivo con BufferedWriter

También se pueden escribir líneas en un archivo de forma eficiente:

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(); // Salto de línea (depende del SO)
            writer.write("Esta es la segunda línea.");
        } catch (IOException e) {
            System.out.println("Error al escribir el archivo: " + e.getMessage());
        }
    }
}

¿Qué ocurre aquí?

Primero creamos un FileWriter y luego lo envolvemos en un BufferedWriter. Cuando llamas a write() o newLine(), los datos no van inmediatamente al disco. Se acumulan en el búfer — una memoria intermedia. Solo cuando el búfer se llena, o cuando cierras el flujo (o llamas explícitamente a flush()), todo el texto acumulado se escribe de una vez en el archivo. Este enfoque acelera la escritura y reduce los accesos al disco.

¿Cómo se ve en una aplicación completa?

Supongamos que escribimos un programa «diario» sencillo que guarda entradas en un archivo y las muestra por pantalla.

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

        // Escritura de una nueva entrada
        System.out.print("Introduzca una nueva entrada en el diario: ");
        String entry = scanner.nextLine();

        try (BufferedWriter writer = new BufferedWriter(new FileWriter(fileName, true))) {
            writer.write(entry);
            writer.newLine();
            System.out.println("¡Entrada guardada!");
        } catch (IOException e) {
            System.out.println("Error al escribir: " + e.getMessage());
        }

        // Lectura de todas las entradas
        System.out.println("\nSu diario:");
        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("Error al leer: " + e.getMessage());
        }
    }
}

Tomamos el nombre de archivo "diary.txt" y proponemos al usuario introducir una nueva entrada. Para guardarla usamos FileWriter en modo append (le pasamos true), de modo que las notas anteriores no se borran: cada línea nueva se añade al final del archivo. La envoltura BufferedWriter hace la escritura rápida y eficiente: los datos primero se acumulan en memoria y luego se envían al disco de una sola vez.

Después abrimos el mismo archivo para lectura con BufferedReader. Toma el contenido en bloques grandes y te entrega las líneas una a una. En el bucle el programa simplemente las muestra por pantalla y, al final, ves todo tu «diario» de principio a fin.

3. Ventajas de BufferedReader y BufferedWriter

Aceleración significativa

Cuando lees o escribes un archivo con flujos con búfer, el programa realiza decenas, e incluso cientos, de veces menos accesos al disco. Esto se nota especialmente en archivos grandes.

Métodos cómodos

  • BufferedReader.readLine() — permite leer el archivo por líneas, lo cual es muy cómodo para procesar archivos de texto (por ejemplo, logs, CSV, archivos de configuración).
  • BufferedWriter.newLine() — añade un salto de línea, insertando el carácter adecuado según el SO.

Simplicidad de uso

  • Las clases se combinan fácilmente con otros flujos (por ejemplo, puedes envolver un InputStreamReader dentro de un BufferedReader para leer archivos con distintas codificaciones).
  • Cierran automáticamente todos los recursos al usar try-with-resources.

Flexibilidad

  • Puedes establecer el tamaño del búfer explícitamente si el valor por defecto (normalmente 8 KB) te parece pequeño o grande:
BufferedReader reader = new BufferedReader(new FileReader("big.txt"), 16384); // búfer de 16 KB

4. Cuándo usar BufferedReader y BufferedWriter

Úsalos:

  • Si trabajas con archivos de texto (logs, CSV, grandes volúmenes de texto).
  • Cuando necesites leer o escribir el archivo línea a línea.
  • Para acelerar el trabajo con archivos grandes cuando la velocidad es importante.
  • Si necesitas procesar un flujo de datos de la red u otra fuente que admita Reader/Writer.

No los uses:

  • Para trabajar con archivos binarios (por ejemplo, imágenes, archivos comprimidos, vídeo) — para ello están InputStream/OutputStream.
  • Si el archivo es muy pequeño y se lee/escribe una sola vez por completo — aquí la ganancia del almacenamiento en búfer será mínima (aunque tampoco perjudica).

5. Matices útiles

Combinación con distintas codificaciones

Si necesitas leer/escribir archivos en una codificación concreta (por ejemplo, "UTF-8", "Windows-1251"), usa InputStreamReader/OutputStreamWriter junto con flujos con búfer:

BufferedReader reader = new BufferedReader(
    new InputStreamReader(new FileInputStream("input.txt"), "UTF-8")
);

BufferedWriter writer = new BufferedWriter(
    new OutputStreamWriter(new FileOutputStream("output.txt"), "UTF-8")
);

Vaciado explícito del búfer

A veces es importante que los datos lleguen seguro al disco (por ejemplo, si escribes un log o un ticket). Para ello, llama a writer.flush(). Normalmente no hace falta, ya que cerrar el flujo vacía el búfer automáticamente.

Tamaño del búfer

Por defecto el búfer es de unos 8 KB. Puedes establecer otro tamaño si sabes con certeza que dará un aumento de rendimiento (por ejemplo, al procesar archivos gigantes).

Comparación: FileReader/FileWriter vs. BufferedReader/BufferedWriter

Clase Velocidad en archivos grandes Comodidad de lectura por líneas Comodidad de escritura por líneas Flexibilidad con codificaciones
FileReader/FileWriter Lento No (solo por carácter) No (solo por carácter) Solo por defecto
BufferedReader/Writer Rápido Sí (readLine()) Sí (newLine()) Sí (mediante InputStreamReader/OutputStreamWriter)

6. Errores típicos al trabajar con BufferedReader y BufferedWriter

Error n.º 1: Olvidar cerrar el flujo. Si no usas try-with-resources o no llamas a close(), el archivo puede quedar bloqueado y los datos no escribirse en el disco. ¡Usa siempre try-with-resources!

Error n.º 2: Confundir el trabajo con archivos de texto y binarios. Intentar abrir un archivo binario (".jpg", ".zip") con BufferedReader llevará a caracteres ilegibles y, muy probablemente, a errores. Para archivos binarios usa InputStream/OutputStream.

Error n.º 3: No usar almacenamiento en búfer con grandes volúmenes de datos. Si lees o escribes carácter a carácter, el programa será lento. Usa siempre almacenamiento en búfer para archivos grandes.

Error n.º 4: No llamar a flush() cuando es necesario. Si necesitas que los datos aparezcan en el disco de inmediato (por ejemplo, para el registro de logs), llama a writer.flush(). Pero normalmente cerrar el flujo hará el trabajo por ti.

Error n.º 5: No tener en cuenta la codificación. Si abres un archivo con una codificación incorrecta, el texto puede mostrarse de forma errónea (las letras se convierten en «?» u otros caracteres). Indica siempre la codificación adecuada si difiere de la del sistema (por ejemplo, "UTF-8").

1
Tarea
JAVA 25 SELF, nivel 36, lección 0
Bloqueada
Extracción del primer pensamiento del pergamino antiguo 📜
Extracción del primer pensamiento del pergamino antiguo 📜
1
Tarea
JAVA 25 SELF, nivel 36, lección 0
Bloqueada
Registro de la gran verdad en los anales de la historia ✍️
Registro de la gran verdad en los anales de la historia ✍️
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION