Flujos de bytes en Java
Lee y escribe datos binarios en Java con InputStream, OutputStream, FileInputStream y FileOutputStream.
El Capítulo 1 introdujo el diseño de java.io como una pila de decoradores: un flujo crudo en la base, capas de funcionalidad envueltas alrededor de él, y la capa más alta exponiendo la API que se llama. Los primeros seis capítulos de esta parte vivían en la cima de esa pila — Files.readString, Files.lines, Files.writeString. Este capítulo desciende un nivel hasta la abstracción orientada a bytes sobre la que se construye toda la pila: InputStream y OutputStream.
Todo archivo, socket, tubería y búfer en memoria de java.io es — en el fondo — un flujo de bytes. Incluso un archivo de texto UTF-8 son bytes en disco; la vista "esto es texto" proviene de un Reader colocado encima de un InputStream. Conocer la API de bytes importa cuando los datos no son texto (imágenes, audio, archivos comprimidos, protocolos de red), cuando necesitas copiar bytes sin decodificarlos, y cuando quieres entender qué hacen realmente las APIs de nivel superior.
El contrato de InputStream
InputStream es una clase abstracta con un solo método. Ese método es:
public abstract int read() throws IOException;Devuelve el siguiente byte como un int en el rango 0..255, o -1 cuando el flujo se agota. El int no es un error: un byte en Java tiene signo (-128..127), pero el contrato del flujo es sin signo, por lo que el tipo de retorno más amplio hace que "fin del flujo" (-1) sea distinguible de un valor de byte real (0xFF se lee como 255, no -1).
Tres métodos más se definen sobre read() y son los que se suelen llamar:
int read(byte[] buf); // read up to buf.length bytes; return count or -1
int read(byte[] buf, int off, int len); // same, into a slice
byte[] readAllBytes(); // Java 9+: read everything into a byte[]
long transferTo(OutputStream out); // Java 9+: pipe straight to a sink, no copy loopreadAllBytes() es la opción cómoda para archivos pequeños; transferTo es la opción cómoda para copiar sin decodificar. Para todo lo demás existe el bucle de lectura con búfer, que es la forma canónica:
byte[] buf = new byte[8192];
int n;
while ((n = in.read(buf)) != -1) {
out.write(buf, 0, n); // n bytes, not buf.length — the last chunk is short
}Dos cosas que hay que interiorizar. Primero, las llamadas a read(byte[]) devuelven cuántos bytes se leyeron realmente, no siempre buf.length. La última lectura casi siempre es parcial; tratar el búfer como lleno corrompe los datos. Segundo, read() y read(byte[]) son bloqueantes — devuelven cuando hay al menos un byte disponible o el flujo termina. No retornan anticipadamente por un disco lento o un socket lento.
Saltar, mirar adelante y retroceder
InputStream también define tres métodos que se usan con menos frecuencia pero que conviene reconocer:
long skip(long n); // discard up to n bytes without copying them anywhere
int available(); // bytes you can read right now without blocking — an estimate, not a length
boolean markSupported();
void mark(int readAheadLimit); // remember this position
void reset(); // jump back to the last markHay dos trampas aquí. available() no es el tamaño del flujo — para un archivo a menudo lo es, pero para un socket es "bytes ya almacenados en el búfer", que puede ser 0 a mitad de la transferencia. Nunca escribas new byte[in.available()] asumiendo que leerás todo. Y mark/reset solo funcionan si markSupported() devuelve true; un FileInputStream crudo devuelve false, así que envuélvelo en un BufferedInputStream (capítulo siguiente) cuando necesites mirar adelante y retroceder.
El contrato de OutputStream
La clase espejo es OutputStream, también con un solo método abstracto:
public abstract void write(int b) throws IOException;Escribe los 8 bits bajos de b e ignora el resto. Las sobrecargas de conveniencia son:
void write(byte[] buf); // write the whole array
void write(byte[] buf, int off, int len); // write a slice — this is the one you usually want
void flush(); // push buffered data to the OS
void close(); // flush + release resourcesflush() solo importa si el flujo tiene búfer. El FileOutputStream crudo no lo tiene — cada write llama al sistema operativo — así que flush no hace nada. BufferedOutputStream (capítulo siguiente) es donde viven el almacenamiento en búfer y la necesidad de hacer flush.
close() llama a flush() primero. Por eso "olvidar cerrar el flujo con búfer" trunca el archivo silenciosamente: el búfer de cola está en memoria esperando un flush que nunca llega.
Flujos de bytes concretos
Las subclases concretas que realmente instanciarás:
| Clase | Qué envuelve |
|---|---|
FileInputStream / FileOutputStream | Un archivo en disco. Abre un descriptor de archivo. |
ByteArrayInputStream / ByteArrayOutputStream | Un byte[] en memoria. Útil para pruebas y para capturar salida. |
BufferedInputStream / BufferedOutputStream | Una vista con búfer de otro flujo. |
PipedInputStream / PipedOutputStream | Una tubería productor/consumidor entre hilos. |
DataInputStream / DataOutputStream | Colocado sobre un flujo de bytes para leer/escribir primitivos de forma portable. |
FileInputStream y FileOutputStream son los flujos de archivo crudos. Son sin búfer: cada read()/write() es una llamada al sistema. Eso es catastrófico para bucles byte a byte — millones de llamadas al sistema — y apenas aceptable para lecturas en bloques con un búfer de 8 KB o más. El capítulo sobre búferes es lo que hace que la API byte a byte sea asequible.
// Raw, unbuffered — fine for chunked reads
try (FileInputStream in = new FileInputStream("photo.jpg")) {
byte[] buf = new byte[8192];
int n;
while ((n = in.read(buf)) != -1) { /* process buf[0..n] */ }
}
// Equivalent one-liner, Java 7+
byte[] all = Files.readAllBytes(Path.of("photo.jpg"));Files.readAllBytes es la llamada correcta para archivos pequeños; para cualquier cosa que no quepan en memoria, el bucle en bloques es la forma segura.
Tres patrones que vale la pena memorizar
Las tres cosas que se hacen una y otra vez con flujos de bytes:
// 1. Copy a file
try (InputStream in = Files.newInputStream(src);
OutputStream out = Files.newOutputStream(dst)) {
in.transferTo(out); // Java 9+: no manual loop
}
// Java 7+ one-liner: Files.copy(src, dst);
// 2. Read everything into memory
byte[] all = Files.readAllBytes(path); // small-file shortcut
// 3. Build a byte[] you don't know the size of in advance
ByteArrayOutputStream baos = new ByteArrayOutputStream();
in.transferTo(baos);
byte[] bytes = baos.toByteArray();ByteArrayOutputStream es el sumidero de bytes que crece a medida que se necesita. Así es como el propio JDK implementa readAllBytes() en flujos cuya longitud no se conoce de antemano. Nunca lanza excepciones en write (hasta que te quedas sin heap) y no tiene semántica de close() que valga la pena considerar, lo que lo convierte en el accesorio de prueba estándar para "capturar lo que produjo este escritor."
Cuándo usar flujos de bytes
La respuesta honesta: cuando los datos no son texto. Cualquier cosa binaria — imágenes, audio, video, archivos comprimidos (.zip, .tar), ejecutables, protocol buffers, formatos de archivo personalizados — son bytes y permanecen como bytes.
Cuando los datos son texto, prefiere el lado de flujos de caracteres (Reader/Writer, capítulo siguiente) o el moderno Files.readString / Files.lines. Leer un archivo de texto como bytes crudos y decodificar a mano es la forma estándar de inventarse un bug de charset — los caracteres multi-byte de UTF-8 se dividen entre llamadas a read() y los reensamblas mal. La capa Reader existe precisamente para que no tengas que pensar en eso.
Un ejemplo completo: copiar, calcular hash y capturar
El programa siguiente ejercita la API de flujos de bytes de principio a fin. Escribe un pequeño archivo binario (un encabezado más algo de payload), lo lee de vuelta en bloques calculando un checksum, lo copia a un segundo archivo con transferTo, y captura otra copia en un ByteArrayOutputStream para ver el sumidero en memoria en acción. Los archivos temporales se limpian solos al salir.
Lo que se puede extraer de la ejecución:
- El lado de escritura usó
Files.newOutputStream— una factoría de estiloFilesque devuelve unOutputStreamsimple. Una vez que lo tienes, la API es la misma que Java ha tenido desde la versión 1.0. La factoría simplemente evita construir unFileOutputStreamy preocuparse por las opciones de apertura. - El bucle de lectura usó
n, nobuf.length, al llamar acrc.update. La razón está en la línea de salida: "read in N chunks." El búfer tenía 256 bytes y el archivo tenía 1004 bytes, así que el último bloque fue corto. Usarbuf.lengthhabría calculado el hash de datos basura más allá de los datos reales. in.transferTo(out)es el bucle de copia probado del JDK. Es mediblemente más rápido que un bucle escrito a mano en la mayoría de las JVM porque puede usar un búfer de 16 KB y omitir las comprobaciones de safepoint, y es una línea en lugar de cinco. Úsalo siempre que de otro modo escribirías un buclewhile ((n = in.read(buf)) != -1)sin otra lógica dentro.ByteArrayOutputStreamse conectó directamente atransferTo. Parece un archivo pero vive en memoria — la misma API. Esa simetría es lo que hace quejava.iosea comprobable: pasa unByteArrayInputStreamcomo fuente, unByteArrayOutputStreamcomo sumidero, y puedes hacer pruebas unitarias de código que "escribe en un archivo" sin tocar el disco.- El bloque final imprimió
255y luego-1. Ese es el contrato:0xFFes un valor de byte válido y se lee como255;-1es el centinela fuera de banda que indica "no hay más bytes." Tratar el valor devuelto como unbyte(en lugar deint) y comparar== -1trataría silenciosamente un0xFFreal como fin del flujo. Almacena siempre el resultado en uninty compara con-1antes de convertir.
Qué viene después
Los bytes son la abstracción correcta para datos binarios. El próximo capítulo, Flujos de caracteres en Java, cubre la jerarquía paralela para texto — Reader y Writer, la conexión con charsets, y por qué "simplemente new FileReader(path)" es la fuente clásica de bugs del tipo "funciona en mi máquina, falla en el servidor."