W3docs

Java DataInput e DataOutput Streams

Leia e escreva tipos primitivos Java em formato binário portátil com DataInputStream e DataOutputStream.

Até agora nesta parte: bytes (brutos ou com buffer) para dados binários arbitrários, caracteres para texto. Há um terceiro caso de uso que os capítulos anteriores não cobrem — escrever um int, double ou boolean Java em um arquivo e lê-lo de volta como o mesmo tipo, em um formato que outra JVM (rodando em um SO diferente, com uma ordem de bytes padrão diferente) concordará.

É para isso que existem DataInputStream e DataOutputStream. Eles são decoradores que ficam em cima de qualquer stream de bytes e adicionam métodos de leitura/escrita tipados: writeInt, writeDouble, writeUTF, readInt, readDouble, readUTF. O formato binário é documentado, fixo, big-endian e portátil em todas as JVMs já distribuídas.

O que você escreve é o que você lê

DataOutputStream expõe um 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 tem os correspondentes readInt, readLong, readUTF, e assim por diante. O contrato é simétrico: escreva um int com writeInt, leia-o de volta com readInt, obtenha o mesmo número, sempre, em toda JVM, em todo sistema operacional.

Três coisas para internalizar:

  1. O formato não tem separadores de campo. Um arquivo com writeInt(42); writeUTF("alice"); writeDouble(3.14) tem 4 + 2 + 5 + 8 = 19 bytes gravados sem marcadores entre eles. Você deve ler na mesma ordem com os mesmos tipos. Não há schema, não há autodescrição, não há recuperação se você adivinhar errado.

  2. writeUTF é UTF-8 modificado. O prefixo é um comprimento de 16 bits sem sinal (portanto máximo de 65.535 bytes por string), e U+0000 é codificado como dois bytes (0xC0 0x80) em vez do byte único padrão. O formato é incompatível com UTF-8 comum — você não pode ler uma string writeUTF com um Reader. Use-o apenas quando ambos os lados são Java.

  3. Big-endian, sempre. A ordem de bytes nativa da máquina varia (x86 é little-endian, protocolos de rede são big-endian), mas DataOutputStream escreve big-endian incondicionalmente. É isso que torna o formato portátil. Se você precisa de little-endian para um protocolo que não controla, use java.nio.ByteBuffer — ele tem uma ordem de bytes configurável.

Quando usar data streams

Dois casos:

  • Você controla ambos os lados e quer um formato binário simples, compacto e portátil entre linguagens. Um "arquivo de save" para um pequeno jogo Java, um arquivo de fixture para um teste unitário, um cache que não precisa sobreviver à versão da JVM. O formato é simples de escrever e analisar; você não precisa de uma biblioteca de serialização.
  • Você está lendo um formato de arquivo que usa o layout de data stream Java. Arquivos de classe (.class), registros formatados com RandomAccessFile, alguns arquivos de índice .jar. Todos foram escritos com DataOutputStream porque o JDK constrói o próprio formato.

Quando você precisa de interoperabilidade entre linguagens (Python, Go, JS), prefira JSON, Protocol Buffers ou MessagePack. Quando precisa de versionamento e evolução de schema, ObjectOutputStream é mais próximo — mas é mais pesado e tem suas próprias armadilhas.

A regra de fim de arquivo

Onde InputStream.read() retorna -1 ao fim do stream, DataInputStream.readInt() (e similares) lança EOFException. Não há sentinela in-band — um int legal pode ter qualquer valor de 32 bits, incluindo -1, então a única maneira de sinalizar o fim do stream é a exceção.

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
  }
}

Esse try/catch para terminação normal é a forma idiomática. É incomum para o JDK fazer de uma exceção um sinal de fluxo de controle, mas a API de leitura tipada não tem outra opção — não há valor a retornar que também não seja um int válido.

Para arquivos onde você controla o formato, o padrão melhor é escrever um prefixo de comprimento no início:

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

Então o lado de leitura percorre n vezes e nunca precisa capturar EOFException para controle de fluxo.

Use buffer antes de decorar

DataInputStream não faz buffer. Cada readInt se torna uma série de chamadas read() no stream subjacente. Se esse stream subjacente for um FileInputStream, cada readInt são quatro syscalls. Sempre envolva com BufferedInputStream primeiro:

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

Essa é a pilha padrão de três níveis: arquivo → com buffer → dados. A mesma ordem se aplica à escrita. Pule o buffer e você paga o custo de syscall por byte do capítulo de streams com buffer, multiplicado pelo número de bytes por primitivo.

Um exemplo prático: um pequeno formato de registro binário

O programa abaixo define um registro binário mínimo — um int id, um nome UTF, um double score, um boolean active — e escreve alguns registros em um arquivo temporário com DataOutputStream. Ele os lê de volta com DataInputStream usando tanto o padrão de prefixo de contagem quanto o padrão de EOFException, e finalmente mostra o modo de falha por incompatibilidade de formato onde o leitor e o escritor discordam nos tipos de campo.

java— editable, runs on the server

O que tirar da execução:

  • O tamanho do arquivo resultou exatamente nos bytes que você preveria somando as larguras tipadas: 4 (contagem) + por registro (4 + UTF com prefixo de comprimento + 8 + 1). Sem preenchimento, sem separadores. Um arquivo de data stream são os bytes gravados, nada mais.
  • Ambos os padrões de leitura produziram os mesmos três registros. O padrão de prefixo de contagem é o melhor quando você está projetando o formato; o padrão de EOFException é o que você usa quando não pode alterar o escritor e o formato é aberto.
  • O bloco de incompatibilidade de formato escreveu dois ints e leu um long. Os bytes no disco (00 00 00 2A 00 00 00 63) eram válidos para qualquer interpretação — DataInputStream não tem como saber. As duas interpretações são mutuamente consistentes byte a byte e mutuamente erradas no nível semântico. Esse é o custo de um formato binário sem schema: a disciplina na fronteira é a única proteção.
  • Cada stream foi envolvido Files.newInputStreamBufferedInputStreamDataInputStream (e o mesmo no lado de escrita). Pule o buffer e readInt vira quatro syscalls; a camada de data stream é puramente conversão de formato e não adiciona buffer próprio.
  • writeUTF foi usado para o nome. O formato é adequado para comunicação inter-Java e inútil para qualquer outra coisa — não o escolha para um arquivo de configuração que você possa um dia ler em Python. Para "apenas Java e quero compacto," é a ferramenta certa; para "outra pessoa pode ler isso," vá para JSON ou Protobuf.

O que vem a seguir

Data streams tratam um primitivo por vez e exigem que o leitor conheça o formato. O próximo capítulo, Java PrintWriter, volta ao lado de caracteres e cobre o decorador Writer que adiciona print, println e printf — a API que você usa em System.out desde o capítulo 1, finalmente como o gravador de arquivos que sempre foi.

Prática

Prática
Um arquivo foi escrito por `DataOutputStream` em um servidor Linux x86 (ordem de bytes nativa little-endian) com `out.writeInt(1)`. O que `DataInputStream.readInt()` retorna em um laptop Windows ARM lendo o mesmo arquivo?
Um arquivo foi escrito por `DataOutputStream` em um servidor Linux x86 (ordem de bytes nativa little-endian) com `out.writeInt(1)`. O que `DataInputStream.readInt()` retorna em um laptop Windows ARM lendo o mesmo arquivo?
Was this page helpful?