W3docs

Java Buffered Streams

Acelere I/O em Java com streams com buffer — BufferedReader, BufferedWriter, BufferedInputStream e BufferedOutputStream.

Os capítulos sobre streams de bytes e de caracteres descreveram as APIs brutas com honestidade: cada chamada a FileInputStream.read() ou FileReader.read() é uma syscall. Uma syscall leva algo da ordem de um microssegundo — rápida isoladamente, catastrófica em um loop apertado. Ler um arquivo de 1 MB byte a byte são um milhão de syscalls; o mesmo arquivo com um buffer de 8 KB são 128. A diferença em tempo real é de duas ou três ordens de magnitude.

Os decoradores Buffered* ficam entre o seu código e o stream bruto. Eles mantêm um byte[] (ou char[]) em memória e atendem chamadas read() a partir dele, indo ao SO apenas quando o buffer está vazio. No lado da escrita, eles acumulam pequenas escritas em um buffer e só fazem write() no SO quando o buffer enche ou você chama flush/close. Mesma API, custo completamente diferente.

As quatro classes com buffer

ClasseEnvolve
BufferedInputStreamUm InputStream. Adiciona um buffer interno byte[].
BufferedOutputStreamUm OutputStream. Adiciona um buffer interno byte[].
BufferedReaderUm Reader. Adiciona um buffer interno char[] e o famoso método readLine().
BufferedWriterUm Writer. Adiciona um buffer interno char[] e um método newLine().

Todas as quatro envolvem qualquer stream do tipo correspondente — arquivo, socket, pipe, em memória — não apenas streams de arquivo:

BufferedInputStream  in  = new BufferedInputStream(new FileInputStream(path.toFile()));
BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream(path.toFile()));
BufferedReader r = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8));
BufferedWriter w = new BufferedWriter(new OutputStreamWriter(out, StandardCharsets.UTF_8));

O tamanho padrão do buffer é 8192 bytes/chars — escolhido para corresponder aos tamanhos de página comuns do SO. Você pode passar um tamanho diferente ao segundo construtor, mas o padrão serve bem em praticamente todos os casos. Buffers maiores não aceleram as coisas linearmente; apenas usam mais memória.

A API moderna entrega esses decoradores já montados:

BufferedReader r = Files.newBufferedReader(path);                            // UTF-8 by default
BufferedWriter w = Files.newBufferedWriter(path, StandardCharsets.UTF_8);
InputStream    in  = new BufferedInputStream(Files.newInputStream(path));
OutputStream   out = new BufferedOutputStream(Files.newOutputStream(path));

Files.newBufferedReader / Files.newBufferedWriter já envolvem a classe bridge com o charset correto e um BufferedReader/BufferedWriter. Para texto, esse é o substituto de uma linha para a pilha manual de três níveis.

BufferedReader.readLine()

O motivo pelo qual BufferedReader é a classe mais usada em java.io:

String readLine() throws IOException;          // a line, terminator stripped, or null at end
Stream<String> lines();                         // Java 8+: line stream

readLine reconhece \n, \r e \r\n como terminadores de linha e retorna a linha sem o terminador. Retorna null (não uma string vazia, não -1) no fim do stream — o idioma padrão de leitura de linha:

try (BufferedReader r = Files.newBufferedReader(path)) {
  String line;
  while ((line = r.readLine()) != null) {
    process(line);
  }
}

r.lines() retorna um Stream<String> para a forma de pipeline funcional. O stream possui o Reader aberto, portanto o try-with-resources em torno do reader ainda faz o trabalho de fechar — o próprio lines() não precisa de seu próprio close.

Duas coisas a saber sobre readLine(). Primeiro, ele aloca uma String por linha. Para loops de processamento de log apertados onde a alocação importa, o read(char[]) de nível mais baixo é o que você quer. Segundo, uma linha vazia é "" (uma string vazia), não null — o arquivo termina apenas quando readLine() retorna null.

BufferedWriter.newLine()

A conveniência espelhada no lado da escrita:

void newLine() throws IOException;             // platform line separator: \n on Unix, \r\n on Windows

newLine() escreve o que a JVM considera o separador de linha da plataforma atual. Isso é uma funcionalidade se você está produzindo arquivos para olhos humanos na máquina local; é um bug se você está produzindo arquivos de dados, arquivos de log ou qualquer coisa destinada a outra máquina. A internet funciona com \n. Sempre escreva \n explicitamente quando a saída precisar ser portável:

w.write("line one\n");                          // portable
w.newLine();                                    // platform-dependent: \n on Unix, \r\n on Windows

O mesmo conselho vale para PrintWriter.println e o especificador de formato %n — eles são dependentes de plataforma. Use-os apenas quando a saída for para consumo local.

A armadilha do "buffer final nunca descarregado"

Este é o bug que toda base de código Java encontra pelo menos uma vez:

// WRONG
BufferedWriter w = Files.newBufferedWriter(path);
w.write("hello");
return;                                          // 'hello' is sitting in the buffer; nothing on disk

Um BufferedWriter não envia bytes ao SO até que o buffer encha ou close() seja executado. Pule o close e o restante se perde — Files.size(path) é 0 e você não faz ideia do porquê. A correção é usar try-with-resources sempre:

try (BufferedWriter w = Files.newBufferedWriter(path)) {
  w.write("hello");
}                                                // close() runs here; tail is flushed

Se você precisa que os dados estejam em disco antes do close — um observador de log-tail, ou outro processo que faz polling no arquivo — chame flush() explicitamente. O buffer não descarrega automaticamente após cada escrita; esse é o preço de ter um buffer.

Mark e reset

BufferedReader e BufferedInputStream suportam uma pequena API de "lookahead e retrocesso":

in.mark(1024);                                   // remember this position; allow up to 1024 bytes of lookahead
int b = in.read();
in.reset();                                      // back to the marked position

Esta é a única API de java.io que permite ler um byte/char e depois colocá-lo de volta. É a base do código de "espiar os primeiros bytes para descobrir o formato" — detecção de BOM UTF-8, sniffing de número mágico, transferência de parser. Sem buffer você não pode fazer isso: os streams brutos não têm mais os bytes depois que foram lidos.

Quando o buffer não ajuda

Dois casos em que adicionar um decorador Buffered* não traz nenhum benefício:

  • A fonte já está em memória. ByteArrayInputStream e StringReader já atendem read() a partir de um byte[]/String em memória; não há syscalls a amortizar.
  • Você está usando Files.readString, Files.readAllBytes, Files.write ou transferTo. Essas chamadas fazem seu próprio I/O em blocos com um grande buffer interno. Envolvê-las em BufferedInputStream é redundante — o JDK já fez o buffer.

O caso em que o buffer ajuda é o original: você está lendo ou escrevendo pequenos fragmentos (um único byte, uma única linha, uma chamada printf) e a fonte/destino é um arquivo real, socket ou pipe.

Um exemplo prático: mesma carga, com e sem buffer

O programa abaixo copia o mesmo blob de 32 KB byte a byte de um arquivo temporário para outro — uma vez com FileInputStream/FileOutputStream brutos, uma vez com BufferedInputStream/BufferedOutputStream, uma vez com transferTo como referência. Os tempos medidos tornam visível o custo do buffer ausente. O exemplo então lê as linhas do arquivo com um BufferedReader e demonstra a armadilha de "esqueceu de descarregar" no lado da escrita.

java— editable, runs on the server

O que extrair da execução:

  • A cópia byte a byte bruta foi ordens de magnitude mais lenta que a com buffer. O corpo do loop era idêntico; a única mudança foi envolver os streams de arquivo em BufferedInputStream/BufferedOutputStream. Essa é a razão de existência desses decoradores — mesma API, vastamente menos syscalls.
  • transferTo foi tão rápido quanto a versão com buffer (ou mais rápido). Para "copiar bytes de A para B sem transformação", transferTo é o que você quer — ele já faz buffer internamente e o JDK ajustou o loop. Recorra a ele antes de escrever o seu próprio.
  • Files.newBufferedReader retornou um BufferedReader diretamente. Observe que nunca escrevemos new BufferedReader(new InputStreamReader(new FileInputStream(...), UTF_8)) — essa pilha de três níveis é o que a factory oculta. readLine() saiu dessa pilha gratuitamente.
  • O writer vazado imprimiu 0 bytes antes de flush(). Esses caracteres estavam no buffer em memória, não em disco. Chamar flush() os empurrou para fora; sem o flush explícito (ou um close() adequado), eles teriam se perdido. É por isso que try-with-resources em torno de writers com buffer não é opcional — é o contrato que torna a escrita visível.
  • O loop BufferedReader.readLine() é a forma de processamento de texto mais comum em Java. Memorize a forma while ((line = r.readLine()) != null): a atribuição-em-condição é idiomática aqui, e o sentinel null (não uma string vazia) é a condição de término do loop.

O que vem a seguir

O buffer resolve o custo de syscall por chamada, mas não muda o que os bytes significam. O próximo capítulo, Java DataInput and DataOutput Streams, aborda os decoradores que leem e escrevem primitivos Java em um formato binário portável — a camada que permite escrever um int em um arquivo e lê-lo de volta como um int em um SO diferente.

Prática

Prática
O que acontece com os dados escritos por `w.write('hello')` se você esquecer de fechar um `BufferedWriter` (e nunca chamar `flush()`)?
O que acontece com os dados escritos por `w.write('hello')` se você esquecer de fechar um `BufferedWriter` (e nunca chamar `flush()`)?
Was this page helpful?