Lectura/Escritura de ficheros binarios
Table of Contents
Leer ficheros binarios
En los ficheros binarios como JPEG o PDF, por ejemplo, los bytes no representan letras, números… no se hace traducción de los bytes a gráficos que representan esos símbolos usando tablas como ASCII o UTF-8. Esto hace que no sea posible, ni tenga sentido, leer estos ficheros línea a línea, porque no existe el símbolo \n en dichos ficheros.
Así pues, lo que podríamos pensar es que la mejor opción para leer ficheros binarios es hacerlo byte a byte, de uno en uno, y buscando de esta manera el significado de estos bytes. Tampoco es lo mejor porque cada lectura de un bytes es un acceso a disco lo que es muy lento y lastraría el rendimiento del programa. Ten en cuenta que pueden haber millones, si no miles de millones, de bytes en un fichero binario.
¿Cómo se deben leer entonces los ficheros binarios? Te muestro 4 opciones que puedes usar en función de los objetivos que busques y/o las necesidades a las que te enfrentes:
a) Leer y cargar todo el fichero binario en un byte[]
- Es una opción válida si el fichero binario es pequeño.
- No es necesario cerrar de forma explícita el fichero, lo hace
Filesautomáticamente.
var ruta = Path.of("/usr/share/datos.bin");
byte[] contenido = Files.readAllBytes(ruta);
b) Leer por bloques de bytes
- Si el fichero es grande podemos cargarlo y procesarlo por bloques de bytes.
- No se usa búfer intermedio de Java.
- Cada vez que se llama a
readse hace una llamada el sistema. - Es necesario cerrar el fichero o hacerlo en un try-with-resource.
Por ejemplo, este programa procesa el fichero en bloques de 8KiB:
var ruta = Path.of("/usr/share/datos.bin");
byte[] buffer = new byte[8192];
int bytesLeidos;
try (InputStream in = Files.newInputStream(ruta)) {
while ((bytesLeidos = in.read(buffer)) != -1) {
// procesar el bloque de bytes leídos y cargados en buffer
}
} catch (IOException e) {
// manejar la excepción
}
c) Leer por bloques de bytes usando búfer intermedio
- Si el fichero es grande podemos cargarlo y procesarlo por bloques de bytes.
- Se usa búfer intermedio en Java lo que permite una mejora en el rendimiento porque se hacen menos llamadas al sistema.
- Es necesario cerrar el fichero o hacerlo en un try-with-resource.
Por ejemplo, este programa procesa el fichero en bloques de 8KiB:
var ruta = Path.of("/usr/share/datos.bin");
byte[] buffer = new byte[8192];
int bytesLeidos;
try (BufferedInputStream bis = new BufferedInputStream(Files.newInputStream(ruta))) {
while ((bytesLeidos = bis.read(buffer)) != -1) {
// procesar el bloque de bytes leídos y cargados en buffer
}
} catch (IOException e) {
// manejar la excepción
}
d) Leer en bloques de tamaño fijo (datos primitivos - estructuras)
Muchos archivos binarios como JPEG, PDF, FIT… están organizados en cabeceras y campos de tamaño fijo. Así, estos se pueden leer solicitando los bytes exactos que ocupa cada tipo de dato.
Imagina que queremos leer un fichero binario cuya estructura es la siguiente:
- 1 byte para la cabecera.
- 4 bytes que codifican un día y hora determinado (un timestamp).
- 2 bytes que codifican la temperatura que hizo ese día a esa hora.
Path ruta = Path.of("/usr/share/datos.bin");
try (DataInputStream dis = new DataInputStream(new BufferedInputStream(Files.newInputStream(ruta)))) {
byte cabecera = dis.readByte(); // Lee 1 byte
int fechaConHora = dis.readInt(); // Lee 4 bytes
short temperatura = dis.readShort(); // Lee 2 bytes
} catch (IOException e) {
// manejar la excepción
}
Escribir ficheros binarios
De manera simétrica a la lectura, al escribir ficheros binarios no enviamos texto ni caracteres traducidos, sino bloques de bytes puros.
Siguiendo la misma lógica que en la lectura, disponemos de los mismos enfoques:
a) Escribir todos los bytes al fichero de golpe
- Cuando tienes todos los datos a escribir: mejor hacerlo de golpe.
- No se usa buffer intermedio.
- La clase de utilidad Files se encarga de abrir el fichero, guardar, hacer flush y cerrar.
var path = Path.of("oswrite.bin");
try {
byte[] buffer = { 0x48, 0x4f, 0x4c, 0x41 };
Files.write(path, buffer);
} catch (IOException e) {
System.err.println(e);
}
b) Escribir el fichero por bloques de bytes
- Se escriben poco a poco, por bloques de bytes.
- No se usa búfer intermedio.
- Se hace una llamada al sistema cada vez que se llama a
write. - Se tiene que abrir el fichero, hacer el flush y cerrar de forma explícita o en un try-with-resource.
var path = Path.of("oswrite.bin");
try (OutputStream os = Files.newOutputStream(path)) {
byte[] buffer = { 0x48, 0x4f, 0x4c, 0x41 };
os.write(buffer);
} catch (IOException e) {
System.err.println(e);
}
c) Escribir el fichero por bloques de bytes con búfer intermedio
- Como el caso anterior por usando búfer intermedio.
- Este búfer intermedio reduce el número de llamadas al sistema.
var path = Path.of("boswrite.bin");
try (BufferedOutputStream bos = new BufferedOutputStream(Files.newOutputStream(path))) {
byte[] buffer = { 0x48, 0x4f, 0x4c, 0x41 };
bos.write(buffer);
} catch (IOException e) {
System.err.println(e);
}
d) Escribir en bloques de tamaño fijo (datos primitivos - estructuras)
- Se escriben datos primitivos o estructuras binarias:
int,short,double… - Se puede usar o no búfer, dependiendo del decorator a usar.
- Se tiene que abrir el fichero, hacer el flush y cerrar de forma explícita o hacerlo en un try-with-resource.
Esta opción se utiliza cuando queremos generar o guardar un fichero binario estructurado (un archivo de configuración propio, exportación de métricas, cabeceras, etc) donde cada dato primitivo de Java debe ocupar su número exacto de bytes en el disco.
Te muestro dos ejemplos. El equivalente a los anteriores:
var path = Path.of("boswrite.bin");
try (DataOutputStream dos = new DataOutputStream(
new BufferedOutputStream(Files.newOutputStream(path)))) {
dos.writeByte(0x48); // Escribe 1 byte
dos.writeShort(0x4f4c); // Escribe 2 bytes
dos.writeByte(0x41); // Escribe 1 byte
} catch (IOException e) {
System.err.println(e);
}
Un nuevo ejemplo donde se pretende escribir, en un fichero binario, una cabecera de 1 byte, una fecha y hora en formato timestamp y una temperatura (1 byte de cabecera, 4 bytes de timestamp y 2 bytes de temperatura):
Path ruta = Path.of("salida.bin");
try (DataOutputStream dos = new DataOutputStream(new BufferedOutputStream(Files.newOutputStream(ruta)))) {
byte cabecera = 0x01;
int fechaConHora = 1700000000; // Epoch timestamp (4 bytes)
short temperatura = 24; // Grados celsius (2 bytes)
dos.writeByte(cabecera); // Escribe 1 byte
dos.writeInt(fechaConHora); // Escribe 4 bytes
dos.writeShort(temperatura); // Escribe 2 bytes
} catch (IOException e) {
// manejar la excepción
}
Resumen lectura/escritura binaria en Java
En la API de Java I/O existe una simetría casi perfecta entre las operaciones de entrada (InputStream / Files) y de salida (OutputStream / Files).
| Escenario | Lectura (Input) | Escritura (Output) | Uso principal / Características |
|---|---|---|---|
| a) De un solo golpe | Files.readAllBytes(path) |
Files.write(path, bytes) |
Lectura o escritura delfichero entero en un byte[] |
| b) Por bloques (Sin búfer Java) | Files.newInputStream(path) |
Files.newOutputStream(path) |
Lectura o escritura por bloques de byte[] sin búfer de Java |
| c) Por bloques (Con búfer Java) | BufferedInputStream |
BufferedOutputStream |
Lectura o escritura por bloques de byte[] usando búfer de Java |
| d) Estructuras / Primitivos | DataInputStream |
DataOutputStream |
Lectura o escritura de fichero binarios con estructura |
Regla práctica de rendimiento:
- Para opciones a) y b) no necesitas añadir un decorador de búfer. En a) la clase
Filesgestiona todo el ciclo de vida, y en b) el propio arraybyte[]que le pasas al métodoread()owrite()hace la función de búfer. - Para la opción d), es una buena práctica interponer un
BufferedInputStream/BufferedOutputStreamentre el fichero y la claseDataInput/OutputStreampara no realizar llamadas al sistema con cada tipo primitivo leído o escrito.