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 loopreadAllBytes() é 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 markDuas 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 resourcesflush() 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á:
| Classe | O que encapsula |
|---|---|
FileInputStream / FileOutputStream | Um arquivo em disco. Abre um descritor de arquivo. |
ByteArrayInputStream / ByteArrayOutputStream | Um byte[] em memória. Útil para testes e para capturar saída. |
BufferedInputStream / BufferedOutputStream | Uma visão com buffer de outro fluxo. |
PipedInputStream / PipedOutputStream | Um pipe produtor/consumidor entre threads. |
DataInputStream / DataOutputStream | Em 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.
O que extrair da execução:
- O lado de gravação usou
Files.newOutputStream— uma fábrica no estiloFilesque retorna umOutputStreamsimples. 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 construirFileOutputStreame se preocupar com opções de abertura. - O laço de leitura usou
n, nãobuf.length, ao chamarcrc.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. Usarbuf.lengthteria 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çowhile ((n = in.read(buf)) != -1)sem outra lógica dentro.ByteArrayOutputStreamse conectou diretamente aotransferTo. Parece um arquivo mas vive em memória — a mesma API. Essa simetria é o que torna ojava.iotestável: passe umByteArrayInputStreampara a fonte, umByteArrayOutputStreampara o destino, e você pode testar unitariamente código que "grava em um arquivo" sem tocar o disco.- O bloco final imprimiu
255e depois-1. Esse é o contrato:0xFFé um valor de byte válido e é lido de volta como255;-1é o sentinela fora de banda que diz "sem mais bytes." Tratar o retorno comobyte(em vez deint) e comparar== -1trataria silenciosamente um0xFFreal como fim do fluxo. Sempre armazene o resultado em uminte compare com-1antes 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."