1. Introducción
Empecemos por lo más sencillo: ¿en qué se diferencia un archivo binario de uno de texto? Un archivo de texto es un archivo que se puede abrir en un bloc de notas corriente y ver letras, números, espacios y otros símbolos. Por ejemplo, my_notes.txt o poem.txt.
Un archivo binario es un archivo que no contiene texto, sino bytes arbitrarios. Puede ser una imagen (.jpg, .png), música (.mp3), un archivo comprimido (.zip), un ejecutable (.exe), un vídeo (.mp4), un archivo de base de datos, etc. Si abres un archivo así en un bloc de notas, verás algo como ÿØÿà o una pared de símbolos incomprensibles. ¡Es normal! El ordenador «entiende» solo bytes: para él, tanto el texto como una imagen o un vídeo son simplemente una secuencia de bytes. En los archivos de texto esos bytes se pueden interpretar como caracteres; en los binarios son datos “en bruto” que no están pensados para ser leídos por una persona.
Clases principales para trabajar con archivos binarios
En Java, para trabajar con archivos binarios se usan flujos de bytes:
- InputStream — clase base para leer bytes.
- OutputStream — clase base para escribir bytes.
Para trabajar con archivos existen implementaciones concretas:
- FileInputStream — lee bytes de un archivo.
- FileOutputStream — escribe bytes en un archivo.
Si has oído hablar de FileReader y FileWriter, ten en cuenta que trabajan con caracteres y solo sirven para texto. Para archivos binarios usa únicamente InputStream/OutputStream y sus descendientes.
2. Lectura de archivos binarios
Lectura byte a byte
La forma más simple es leer el archivo byte a byte. Es didáctico, pero muy lento.
try (FileInputStream in = new FileInputStream("image.jpg")) {
int b;
while ((b = in.read()) != -1) {
// b es un número de 0 a 255 (byte); -1 significa fin de archivo
// Puedes procesar el byte, por ejemplo, sumar todos los bytes
}
}
El método read() devuelve el siguiente byte como int (de 0 a 255) y, cuando el archivo termina, devuelve -1. Normalmente se lee byte a byte solo cuando se necesita algo muy específico (por ejemplo, analizar la estructura del archivo).
Lectura por bloques (con búfer)
Leer byte a byte es como ir a la tienda por cada manzana por separado. ¡Es mucho más eficiente llevarte una bolsa entera de una vez! En Java existe el método read(byte[] buffer), que llena un array con bytes del archivo.
try (FileInputStream in = new FileInputStream("image.jpg")) {
byte[] buffer = new byte[4096]; // búfer de 4 KB
int bytesRead;
while ((bytesRead = in.read(buffer)) != -1) {
// buffer contiene bytesRead bytes del archivo
// Puedes procesar estos bytes, por ejemplo, guardarlos en otro sitio
}
}
El método read(buffer) devuelve cuántos bytes se han leído realmente (puede ser menor que el tamaño del búfer, especialmente en la última lectura). Este enfoque es mucho más rápido porque hay menos accesos al disco.
Ejemplo: copiar un archivo
Escribamos un programa sencillo que copie cualquier archivo binario (por ejemplo, una imagen) de un lugar a otro.
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
public class BinaryCopyExample {
public static void main(String[] args) {
String source = "cat.jpg";
String dest = "cat_copy.jpg";
try (FileInputStream in = new FileInputStream(source);
FileOutputStream out = new FileOutputStream(dest)) {
byte[] buffer = new byte[8192]; // 8 KB: un tamaño óptimo para la mayoría de los casos
int bytesRead;
while ((bytesRead = in.read(buffer)) != -1) {
out.write(buffer, 0, bytesRead);
}
System.out.println("¡La copia ha finalizado!");
} catch (IOException e) {
System.out.println("Error al copiar: " + e.getMessage());
}
}
}
Es sencillo: leemos bloques del archivo de origen y los escribimos de inmediato en el nuevo archivo. Este método funciona con cualquier tipo de archivo: imágenes, archivos comprimidos, vídeos.
3. Escritura de archivos binarios
Escritura de un array de bytes
Si tienes un array de bytes (por ejemplo, lo has recibido de la red o lo has generado en el programa), puedes escribirlo en un archivo así:
byte[] data = new byte[] {1, 2, 3, 4, 5}; // ejemplo de array
try (FileOutputStream out = new FileOutputStream("data.bin")) {
out.write(data); // escribe todo el array en el archivo
}
El método write(byte[]) escribe todos los bytes del array. También puedes escribir solo una parte del array: out.write(data, offset, length).
Escritura de un archivo por partes (por ejemplo, al copiar)
Como en la lectura, normalmente se usa un búfer:
try (FileInputStream in = new FileInputStream("source.bin");
FileOutputStream out = new FileOutputStream("dest.bin")) {
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = in.read(buffer)) != -1) {
out.write(buffer, 0, bytesRead);
}
}
Aquí, todo lo que leemos de un archivo lo escribimos inmediatamente en otro. Este tipo de código es habitual en programas de compresión, gestores de descargas, procesadores de imágenes, etc.
4. Matices útiles
¿Por qué no se debe usar Reader/Writer con archivos binarios?
Reader y Writer trabajan con caracteres (char), no con bytes. Convierten automáticamente bytes a caracteres según la codificación (por ejemplo, UTF-8). Esto es cómodo para texto, pero para archivos binarios ¡es peligrosísimo!
Si intentas escribir una imagen con FileWriter, obtendrás un archivo corrupto que no se podrá abrir. Recuerda: para cualquier archivo no textual usa únicamente InputStream/OutputStream y sus descendientes.
Diferencias y matices importantes al trabajar con archivos binarios
- Tamaño del búfer: Un búfer demasiado pequeño ralentizará el trabajo (demasiados accesos al disco); uno demasiado grande consumirá memoria innecesariamente. 4–16 KB suele ser óptimo.
- Gestión de errores: Maneja siempre IOException: el archivo puede no existir, estar bloqueado o quedarse sin espacio en disco.
- Cierre de flujos: Usa try-with-resources: garantiza el cierre de los archivos incluso si hay errores.
- Sobrescritura del archivo: Si abres un archivo con new FileOutputStream("file.bin"), se sobrescribirá. Para añadir al final, usa el constructor con el parámetro append = true.
- Permisos de acceso: Si el programa no puede abrir el archivo, comprueba los permisos de lectura/escritura.
- readAllBytes(): Permite leer todo el archivo en un array de bytes de una sola vez. Para archivos grandes, no lo uses para no agotar toda la memoria.
5. Errores típicos al trabajar con archivos binarios
Error n.º 1: Usar FileReader/FileWriter para archivos binarios. Esto llevará a la corrupción de los datos, porque estas clases convierten bytes en caracteres y viceversa, lo que para imágenes, archivos comprimidos, etc., es catastrófico.
Error n.º 2: Ignorar el valor de retorno de read(). El método read(byte[]) puede leer menos bytes de los que pides, especialmente en el último bloque. Usa siempre el valor devuelto para saber cuántos bytes se han procesado realmente.
Error n.º 3: Olvidar cerrar el flujo. Si no cierras el archivo, puede permanecer bloqueado y los datos pueden no escribirse por completo (especialmente al escribir). Usa try-with-resources.
Error n.º 4: Intentar leer el archivo completo en memoria sin tener en cuenta su tamaño. Para archivos grandes esto provocará OutOfMemoryError. Usa un búfer y lee por partes.
Error n.º 5: No manejar excepciones. Trabajar con archivos siempre puede producir errores: archivo no encontrado, falta de permisos, disco lleno. No olvides manejar IOException.
GO TO FULL VERSION