Cuando comenzamos a trabajar con entrada y salida de datos en Java, aparecen rápidamente conceptos como InputStream, OutputStream, BufferedInputStream y BufferedOutputStream.
Aunque al principio pueden parecer conceptos diferentes y difíciles de relacionar, la idea fundamental es bastante sencilla:
El Stream representa el flujo de datos y el Buffer actúa como una zona de almacenamiento temporal que permite trabajar con esos datos de forma más eficiente.
¿Qué es un Stream?
Un Stream o flujo representa un canal por el que circulan datos.
Por ejemplo, si queremos leer un archivo:
FileInputStream entrada =
new FileInputStream("datos.txt");
Podemos imaginarlo de esta forma:
┌──────────────┐
│ datos.txt │
└──────┬───────┘
│
│ datos
▼
┌──────────────┐
│ InputStream │
└──────┬───────┘
│
▼
Programa Java
El InputStream permite leer datos.
Para escribir datos utilizamos normalmente un OutputStream:
Programa Java
│
│ datos
▼
┌───────────────┐
│ OutputStream │
└───────┬───────┘
│
▼
Archivo
Por tanto:
InputStream→ entrada de datos.OutputStream→ salida de datos.
¿Qué es un Buffer?
Un buffer es una zona de memoria utilizada para almacenar temporalmente datos.
Podemos imaginarlo como un pequeño depósito.
Por ejemplo:
Archivo
│
│ datos
▼
┌─────────────────────┐
│ BUFFER │
│ │
│ 10101010 │
│ 11001001 │
│ 00110110 │
│ 10010101 │
└─────────┬───────────┘
│
▼
Programa Java
En lugar de estar accediendo constantemente al archivo para obtener cada dato, se puede trabajar con bloques de información almacenados temporalmente en memoria.
Stream + Buffer
Aquí está la relación importante.
Podemos utilizar un BufferedInputStream sobre un FileInputStream:
FileInputStream fis =
new FileInputStream("datos.txt");
BufferedInputStream bis =
new BufferedInputStream(fis);
Tenemos:
Archivo
│
▼
FileInputStream
│
▼
BufferedInputStream
│
▼
Programa
El FileInputStream proporciona el acceso al archivo.
El BufferedInputStream añade un buffer para hacer más eficiente la lectura.
¿Por qué utilizar un buffer?
Imaginemos que queremos leer un archivo de 1 MB.
Sin buffering, determinadas operaciones pueden implicar muchas interacciones con el sistema de entrada/salida.
Podemos representarlo de forma simplificada:
Programa
│
├──────> Archivo
│
├──────> Archivo
│
├──────> Archivo
│
├──────> Archivo
│
└──────> Archivo
Con un buffer:
Programa
│
▼
┌──────────────┐
│ BUFFER │
└──────┬───────┘
│
│ bloque de datos
▼
Archivo
El sistema puede leer un bloque de datos y almacenarlo temporalmente en memoria.
Después, el programa consume los datos desde ese buffer.
BufferedInputStream
Java proporciona la clase:
BufferedInputStream
que permite añadir buffering a un InputStream.
Ejemplo:
import java.io.*;
public class Ejemplo {
public static void main(String[] args) throws IOException {
FileInputStream fis =
new FileInputStream("datos.txt");
BufferedInputStream bis =
new BufferedInputStream(fis);
int dato;
while ((dato = bis.read()) != -1) {
System.out.print((char) dato);
}
bis.close();
}
}
La parte importante es:
BufferedInputStream bis =
new BufferedInputStream(fis);
Estamos envolviendo un stream con otro objeto.
Esto es muy habitual en Java.
¿Qué significa «envolver» un Stream?
Podemos pensar que cada objeto añade una capacidad.
Por ejemplo:
FileInputStream fis =
new FileInputStream("datos.txt");
BufferedInputStream bis =
new BufferedInputStream(fis);
Tenemos:
┌───────────────────────────┐
│ BufferedInputStream │
│ │
│ ┌───────────────────┐ │
│ │ FileInputStream │ │
│ │ │ │
│ │ datos.txt │ │
│ └───────────────────┘ │
│ │
└───────────────────────────┘
El BufferedInputStream utiliza internamente el FileInputStream para obtener los datos.
Por eso podemos construir cadenas de objetos de entrada y salida.
También existe BufferedOutputStream
El mismo concepto se aplica cuando queremos escribir.
Por ejemplo:
FileOutputStream fos =
new FileOutputStream("salida.txt");
BufferedOutputStream bos =
new BufferedOutputStream(fos);
Ahora tenemos:
Programa
│
▼
BufferedOutputStream
│
│ buffer
▼
FileOutputStream
│
▼
salida.txt
El programa escribe datos en el buffer.
Cuando corresponde, esos datos se envían al archivo.
Un detalle importante: flush()
Cuando utilizamos buffers para escribir, aparece otro concepto importante:
flush()
Por ejemplo:
bos.write("Hola".getBytes());
bos.flush();
flush() fuerza a que los datos que todavía están almacenados en el buffer se envíen al destino.
Por eso, al trabajar con streams de salida con buffering es importante cerrar correctamente el stream.
Por ejemplo:
bos.close();
Al cerrar correctamente el stream, Java realiza las operaciones necesarias para vaciar el buffer antes de finalizar la comunicación.
Una comparación sencilla
Podemos imaginar que estamos transportando agua.
El Stream sería la tubería:
Origen ====================> Destino
El Buffer sería un pequeño depósito colocado en medio:
Origen
│
▼
╔══════════════╗
║ BUFFER ║
║ ║
╚══════════════╝
│
▼
Destino
La tubería permite que los datos circulen.
El depósito permite acumular temporalmente una cantidad de datos.
InputStream y OutputStream
Una forma sencilla de diferenciarlos es:
| Clase | Función |
|---|---|
InputStream | Leer datos |
OutputStream | Escribir datos |
BufferedInputStream | Leer utilizando un buffer |
BufferedOutputStream | Escribir utilizando un buffer |
Podemos visualizarlo:
ENTRADA
Archivo ────────> InputStream ────────> Programa
▲
│
BufferedInputStream
Y para salida:
SALIDA
Programa ────────> OutputStream ────────> Archivo
▲
│
BufferedOutputStream
¿El Buffer es otro Stream?
Esta es una duda bastante habitual.
BufferedInputStream sí es un InputStream, porque hereda de él.
Por eso podemos hacer:
InputStream entrada =
new BufferedInputStream(
new FileInputStream("datos.txt")
);
El programa puede trabajar con:
InputStream
sin necesitar conocer todos los detalles internos.
Esto es un ejemplo del uso de la herencia y el polimorfismo en Java.
La idea que debemos recordar
Si estamos empezando con Streams en Java, podemos quedarnos con este esquema:
STREAM
│
flujo de datos
│
┌───────────┴───────────┐
│ │
ENTRADA SALIDA
│ │
InputStream OutputStream
│ │
▼ ▼
BufferedInputStream BufferedOutputStream
│ │
▼ ▼
BUFFER BUFFER
La idea fundamental es:
El Stream proporciona el mecanismo para leer o escribir datos. El Buffer almacena temporalmente esos datos para reducir operaciones de entrada/salida y mejorar el rendimiento.
Ejemplo completo
Un ejemplo sencillo para leer un archivo utilizando buffering:
import java.io.*;
public class LeerArchivo {
public static void main(String[] args) {
try (
BufferedInputStream entrada =
new BufferedInputStream(
new FileInputStream("datos.txt")
)
) {
int dato;
while ((dato = entrada.read()) != -1) {
System.out.print((char) dato);
}
} catch (IOException e) {
System.out.println("Error: " + e.getMessage());
}
}
}
Aquí tenemos varias capas:
datos.txt
│
▼
FileInputStream
│
▼
BufferedInputStream
│
▼
programa
El concepto clave para los alumnos sería:
FileInputStream → permite acceder al archivo.
BufferedInputStream → añade un buffer para trabajar de forma más eficiente.
read() → obtiene los datos.
Conclusión
Los Streams son uno de los mecanismos fundamentales de Java para trabajar con datos procedentes de archivos, redes, dispositivos y otros orígenes.
Los buffers añaden una capa intermedia que permite almacenar temporalmente bloques de datos, reduciendo determinadas operaciones de entrada/salida y mejorando el rendimiento.
Por eso es muy habitual encontrar construcciones como:
BufferedInputStream(
new FileInputStream(...)
)
o:
BufferedOutputStream(
new FileOutputStream(...)
)
La imagen mental más sencilla es:
Stream = canal por el que circulan los datos.
Buffer = zona temporal donde se almacenan datos durante ese flujo.