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").
GO TO FULL VERSION