W3docs

Fluxos de Bytes em Java

Leia e grave dados binários em Java com InputStream, OutputStream, FileInputStream e FileOutputStream.

O Capítulo 1 apresentou o design do java.io como uma pilha de decoradores: um fluxo bruto na base, camadas de funcionalidade envolvidas ao redor, a camada mais alta expondo a API que você chama. Os primeiros seis capítulos desta parte viveram no topo dessa pilha — Files.readString, Files.lines, Files.writeString. Este capítulo desce uma camada para a abstração orientada a bytes sobre a qual toda a pilha é construída: InputStream e OutputStream.

Cada arquivo, socket, pipe e buffer em memória no java.io é — no fundo — um fluxo de bytes. Mesmo um arquivo de texto UTF-8 é bytes em disco; a visão "isso é texto" vem de um Reader em camada sobre um InputStream. Conhecer a API de bytes importa quando os dados não são texto (imagens, áudio, arquivos compactados, protocolos de rede), quando você precisa copiar bytes sem decodificá-los, e quando quer entender o que as APIs de nível mais alto realmente fazem.

O contrato de InputStream

InputStream é uma classe abstrata com um único método. O método é:

public abstract int read() throws IOException;

Ele retorna o próximo byte como um int no intervalo 0..255, ou -1 quando o fluxo se esgota. O int não é um erro: um byte em Java é com sinal (-128..127), mas o contrato do fluxo é sem sinal, então o tipo de retorno mais amplo torna "fim do fluxo" (-1) distinguível de um valor real de byte (0xFF é lido de volta como 255, não -1).

Três métodos adicionais são definidos sobre read() e são os que você geralmente chama:

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 loop

readAllBytes() é a conveniência para arquivos pequenos; transferTo é a conveniência para copiar sem decodificar. Para todo o resto, há o laço de leitura em buffer, que é a 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
}

Duas coisas para internalizar. Primeiro, as chamadas read(byte[]) retornam quantos bytes foram realmente lidos, não sempre buf.length. A última leitura é quase sempre parcial; tratar o buffer como cheio corrompe os dados. Segundo, read() e read(byte[]) são bloqueantes — eles retornam quando pelo menos um byte está disponível ou o fluxo termina. Eles não retornam antecipadamente em um disco lento ou em um socket lento.

Pulando, espiando e retrocedendo

InputStream também define três métodos que você usa com menos frequência, mas deve reconhecer:

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 mark

Duas armadilhas existem aqui. available() não é o tamanho do fluxo — para um arquivo geralmente é, mas para um socket é "bytes já armazenados em buffer," que pode ser 0 no meio de uma transferência. Nunca escreva new byte[in.available()] e assuma que leu tudo. E mark/reset só funcionam se markSupported() retornar true; um FileInputStream bruto retorna false, então envolva-o em um BufferedInputStream (próximo capítulo) quando precisar olhar à frente e voltar.

O contrato de OutputStream

A classe espelhada é OutputStream, também com um único método abstrato:

public abstract void write(int b) throws IOException;

Ele grava os 8 bits inferiores de b e ignora o resto. As sobrecargas de conveniência são:

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 resources

flush() só importa se o fluxo armazena em buffer. O FileOutputStream bruto não — cada write chama o sistema operacional — então flush é uma operação vazia. BufferedOutputStream (próximo capítulo) é onde o buffering, e a necessidade de dar flush, residem.

close() chama flush() primeiro. É por isso que "esqueceu de fechar o fluxo em buffer" trunca silenciosamente o arquivo: o buffer final está na memória aguardando um flush que nunca chega.

Fluxos de bytes concretos

As subclasses concretas que você realmente instanciará:

ClasseO que encapsula
FileInputStream / FileOutputStreamUm arquivo em disco. Abre um descritor de arquivo.
ByteArrayInputStream / ByteArrayOutputStreamUm byte[] em memória. Útil para testes e para capturar saída.
BufferedInputStream / BufferedOutputStreamUma visão com buffer de outro fluxo.
PipedInputStream / PipedOutputStreamUm pipe produtor/consumidor entre threads.
DataInputStream / DataOutputStreamEm camada sobre um fluxo de bytes para ler/gravar primitivos de forma portável.

FileInputStream e FileOutputStream são os fluxos de arquivo brutos. Eles são sem buffer: cada read()/write() é uma chamada de sistema. Isso é catastrófico para laços byte a byte — milhões de chamadas de sistema — e apenas aceitável para leituras em blocos com um buffer de 8 KB ou maior. O capítulo sobre buffering é o que torna a API byte a byte viável.

// 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 é a chamada certa para arquivos pequenos; para qualquer coisa que possa não caber na memória, o laço em blocos é a forma segura.

Três padrões que valem memorizar

As três coisas que você faz com fluxos de bytes repetidamente:

// 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 é o destino de bytes que "cresce conforme você vai." É como o próprio JDK implementa readAllBytes() em fluxos cujo comprimento não é conhecido antecipadamente. Ele nunca lança exceção no write (até você ficar sem heap) e não tem semânticas de close() que valham pensar, o que o torna o fixture de teste padrão para "capturar o que este escritor produziu."

Quando usar fluxos de bytes

A resposta honesta: quando os dados não são texto. Qualquer coisa binária — imagens, áudio, vídeo, arquivos compactados (.zip, .tar), executáveis, protocol buffers, formatos de arquivo personalizados — são bytes e permanecem bytes.

Quando os dados são texto, prefira o lado de fluxo de caracteres (Reader/Writer, próximo capítulo) ou o moderno Files.readString / Files.lines. Ler um arquivo de texto como bytes brutos e decodificar manualmente é a forma padrão de inventar seu próprio bug de charset — caracteres multibyte UTF-8 são divididos entre chamadas read() e você os remonta incorretamente. A camada Reader existe precisamente para que você não precise pensar nisso.

Um exemplo trabalhado: copiar, calcular hash e capturar

O programa abaixo exercita a API de fluxo de bytes de ponta a ponta. Ele grava um pequeno arquivo binário (um cabeçalho mais alguma carga útil), lê de volta pedaço por pedaço em um checksum, copia para um segundo arquivo com transferTo, e captura outra cópia em um ByteArrayOutputStream para você ver o destino em memória em ação. Os arquivos temporários se limpam sozinhos ao sair.

java— editable, runs on the server

O que extrair da execução:

  • O lado de gravação usou Files.newOutputStream — uma fábrica no estilo Files que retorna um OutputStream simples. Uma vez que você o tem, a API é a mesma que Java tem desde a versão 1.0. A fábrica apenas poupa você de construir FileOutputStream e se preocupar com opções de abertura.
  • O laço de leitura usou n, não buf.length, ao chamar crc.update. A razão está na linha de saída: "read in N chunks." O buffer tinha 256 bytes e o arquivo tinha 1004 bytes, então o último bloco foi curto. Usar buf.length teria calculado o hash de lixo após os dados reais.
  • in.transferTo(out) é o laço de cópia testado do JDK. É mensuravelmente mais rápido do que um laço escrito à mão na maioria das JVMs porque pode usar um buffer de 16 KB e pular as verificações de safepoint, e é uma linha em vez de cinco. Use-o sempre que de outra forma escreveria um laço while ((n = in.read(buf)) != -1) sem outra lógica dentro.
  • ByteArrayOutputStream se conectou diretamente ao transferTo. Parece um arquivo mas vive em memória — a mesma API. Essa simetria é o que torna o java.io testável: passe um ByteArrayInputStream para a fonte, um ByteArrayOutputStream para o destino, e você pode testar unitariamente código que "grava em um arquivo" sem tocar o disco.
  • O bloco final imprimiu 255 e depois -1. Esse é o contrato: 0xFF é um valor de byte válido e é lido de volta como 255; -1 é o sentinela fora de banda que diz "sem mais bytes." Tratar o retorno como byte (em vez de int) e comparar == -1 trataria silenciosamente um 0xFF real como fim do fluxo. Sempre armazene o resultado em um int e compare com -1 antes de fazer o cast.

O que vem a seguir

Bytes são a abstração certa para dados binários. O próximo capítulo, Fluxos de Caracteres em Java, cobre a hierarquia paralela para texto — Reader e Writer, ponte de charset, e por que "apenas new FileReader(path)" é a fonte clássica de bugs "funciona na minha máquina, quebrado no servidor."

Prática

Prática
O que `InputStream.read()` retorna quando o fluxo contém um único byte com valor `0xFF`, e o que retorna na próxima chamada?
O que `InputStream.read()` retorna quando o fluxo contém um único byte com valor `0xFF`, e o que retorna na próxima chamada?
Prática
No laço `while ((n = in.read(buf)) != -1) out.write(buf, 0, n);`, por que passar `n` em vez de `buf.length` para `write`?
No laço `while ((n = in.read(buf)) != -1) out.write(buf, 0, n);`, por que passar `n` em vez de `buf.length` para `write`?
Was this page helpful?