W3docs

Flujos DataInput y DataOutput en Java

Lee y escribe tipos primitivos de Java en formato binario portable con DataInputStream y DataOutputStream.

Hasta ahora en esta parte: bytes (crudos o con buffer) para datos binarios arbitrarios, caracteres para texto. Hay un tercer caso de uso que los capítulos anteriores no cubren: escribir un int, double o boolean de Java en un archivo y volver a leerlo como el mismo tipo, en un formato con el que otra JVM (ejecutándose en un sistema operativo diferente, con un orden de bytes predeterminado distinto) estará de acuerdo.

Para eso existen DataInputStream y DataOutputStream. Son decoradores que se colocan sobre cualquier flujo de bytes y añaden métodos de lectura/escritura tipados: writeInt, writeDouble, writeUTF, readInt, readDouble, readUTF. El formato binario está documentado, es fijo, big-endian y portable en todas las JVM que se han publicado.

Lo que escribes es lo que lees

DataOutputStream expone un método por tipo primitivo:

void writeBoolean(boolean v);    //  1 byte (0 or 1)
void writeByte(int v);            //  1 byte (low 8 bits)
void writeShort(int v);           //  2 bytes, big-endian
void writeChar(int v);            //  2 bytes, big-endian (UTF-16 code unit)
void writeInt(int v);             //  4 bytes, big-endian
void writeLong(long v);           //  8 bytes, big-endian
void writeFloat(float v);         //  4 bytes, IEEE 754
void writeDouble(double v);       //  8 bytes, IEEE 754
void writeUTF(String s);          //  modified UTF-8 with a 2-byte length prefix

DataInputStream tiene los correspondientes readInt, readLong, readUTF, y así sucesivamente. El contrato es simétrico: escribe un int con writeInt, léelo de vuelta con readInt, obtienes el mismo número, siempre, en cada JVM, en cada sistema operativo.

Tres cosas que hay que interiorizar:

  1. El formato no tiene separadores de campo. Un archivo con writeInt(42); writeUTF("alice"); writeDouble(3.14) ocupa 4 + 2 + 5 + 8 = 19 bytes escritos sin marcadores entre ellos. Debes leer en el mismo orden con los mismos tipos. No hay esquema, no hay autodescripción, no hay recuperación si lo adivinas mal.

  2. writeUTF es UTF-8 modificado. El prefijo es una longitud sin signo de 16 bits (máximo 65.535 bytes por string), y U+0000 se codifica como dos bytes (0xC0 0x80) en lugar del byte estándar. El formato es incompatible con UTF-8 plano — no puedes leer un string de writeUTF con un Reader. Úsalo solo cuando ambos lados son Java.

  3. Big-endian, siempre. El orden de bytes nativo de la máquina varía (x86 es little-endian, los protocolos de red son big-endian), pero DataOutputStream escribe big-endian incondicionalmente. Eso es lo que hace que el formato sea portable. Si necesitas little-endian para un protocolo que no controlas, usa java.nio.ByteBuffer en su lugar — tiene un orden de bytes configurable.

Cuándo usar flujos de datos

Dos casos:

  • Controlas ambos lados y quieres un formato binario simple, compacto y portable entre lenguajes. Un "archivo de guardado" para un pequeño juego en Java, un archivo de fixtures para una prueba unitaria, una caché que no necesita sobrevivir a la versión de la JVM. El formato es sencillo de escribir y analizar; no necesitas incluir una biblioteca de serialización.
  • Estás leyendo un formato de archivo que usa el esquema de flujo de datos de Java. Los archivos de clase (.class), los registros formateados con RandomAccessFile, algunos archivos de índice de .jar. Todos fueron escritos con DataOutputStream porque el JDK construye el formato en sí.

Cuando necesitas interoperabilidad entre lenguajes (Python, Go, JS), usa JSON, Protocol Buffers o MessagePack. Cuando necesitas versionado y evolución de esquema, ObjectOutputStream es más cercano — pero es más pesado y tiene sus propios problemas.

La regla del fin de archivo

Donde InputStream.read() devuelve -1 al final del flujo, DataInputStream.readInt() (y sus equivalentes) lanza EOFException. No hay centinela dentro de banda — un int legal puede ser cualquier valor de 32 bits, incluyendo -1, así que la única forma de señalar el fin del flujo es la excepción.

try (DataInputStream in = new DataInputStream(new BufferedInputStream(Files.newInputStream(path)))) {
  try {
    while (true) {
      int x = in.readInt();
      process(x);
    }
  } catch (EOFException e) {
    // normal end of stream
  }
}

Ese try/catch para la terminación normal es la forma idiomática. Es inusual que el JDK convierta una señal de flujo de control en una excepción, pero la API de lectura tipada no tiene otra opción — no hay ningún valor que devolver que no sea también un int válido.

Para archivos donde controlas el formato, el patrón mejor es escribir un prefijo de longitud al principio:

out.writeInt(n);
for (int i = 0; i < n; i++) out.writeInt(values[i]);

Así el lado lector hace un bucle n veces y nunca tiene que capturar EOFException para el flujo de control.

Aplica buffer antes de decorar

DataInputStream no aplica buffer. Cada readInt se convierte en una serie de llamadas read() en el flujo subyacente. Si ese flujo subyacente es un FileInputStream, cada readInt equivale a cuatro llamadas al sistema. Envuelve siempre con BufferedInputStream primero:

// Right
DataInputStream  in  = new DataInputStream(new BufferedInputStream(Files.newInputStream(path)));
DataOutputStream out = new DataOutputStream(new BufferedOutputStream(Files.newOutputStream(path)));

Esa es la pila estándar de tres niveles: archivo → con buffer → datos. El mismo orden aplica para escritura. Omite el buffer y pagarás el coste de llamada al sistema por byte del capítulo de flujos con buffer, multiplicado por el número de bytes por primitivo.

Un ejemplo completo: un pequeño formato de registro binario

El programa a continuación define un registro binario mínimo — un id de tipo int, un nombre UTF, una score de tipo double, un active de tipo boolean — y escribe algunos registros en un archivo temporal con DataOutputStream. Los lee de vuelta con DataInputStream usando tanto el patrón de prefijo de conteo como el patrón de EOFException, y finalmente muestra el modo de fallo por desajuste de formato cuando el lector y el escritor no coinciden en los tipos de campo.

java— editable, runs on the server

Lo que hay que extraer de la ejecución:

  • El tamaño del archivo resultó ser exactamente los bytes que predecirías sumando los anchos tipados: 4 (conteo) + por registro (4 + UTF con prefijo de longitud + 8 + 1). Sin relleno, sin separadores. Un archivo de flujo de datos son los bytes escritos, nada más.
  • Ambos patrones de lectura produjeron los mismos tres registros. El patrón de prefijo de conteo es el mejor cuando diseñas el formato; el patrón de EOFException es al que recurres cuando no puedes cambiar el escritor y el formato es abierto.
  • El bloque de desajuste de formato escribió dos ints y leyó un long. Los bytes en disco (00 00 00 2A 00 00 00 63) eran válidos para cualquiera de las dos interpretaciones — DataInputStream no tiene forma de saberlo. Las dos interpretaciones son mutuamente consistentes byte a byte y mutuamente incorrectas a nivel semántico. Ese es el coste de un formato binario sin esquema: la disciplina en la frontera es la única protección.
  • Cada flujo estaba envuelto como Files.newInputStreamBufferedInputStreamDataInputStream (y lo mismo en el lado de escritura). Omite el buffer y readInt se convierte en cuatro llamadas al sistema; la capa de flujo de datos es pura conversión de formato y no añade buffer propio.
  • Se usó writeUTF para el nombre. El formato es válido para comunicación entre Java y es inútil para cualquier otra cosa — no lo elijas para un archivo de configuración que algún día podrías leer en Python. Para "solo Java y quiero que sea pequeño", es la herramienta correcta; para "alguien más podría leer esto", usa JSON o Protobuf.

Qué sigue

Los flujos de datos manejan un primitivo a la vez y requieren que el lector conozca el formato. El próximo capítulo, Java PrintWriter, vuelve al lado de los caracteres y cubre el decorador Writer que añade print, println y printf — la API que has estado usando en System.out desde el capítulo 1, finalmente como el escritor de archivos que siempre fue.

Práctica

Práctica
Un archivo fue escrito por `DataOutputStream` en un servidor Linux x86 (orden de bytes nativo little-endian) con `out.writeInt(1)`. ¿Qué devuelve `DataInputStream.readInt()` en un portátil Windows ARM que lee el mismo archivo?
Un archivo fue escrito por `DataOutputStream` en un servidor Linux x86 (orden de bytes nativo little-endian) con `out.writeInt(1)`. ¿Qué devuelve `DataInputStream.readInt()` en un portátil Windows ARM que lee el mismo archivo?
Was this page helpful?