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
| Classe | Envolve |
|---|---|
BufferedInputStream | Um InputStream. Adiciona um buffer interno byte[]. |
BufferedOutputStream | Um OutputStream. Adiciona um buffer interno byte[]. |
BufferedReader | Um Reader. Adiciona um buffer interno char[] e o famoso método readLine(). |
BufferedWriter | Um 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 streamreadLine 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 WindowsnewLine() 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 WindowsO 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 diskUm 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 flushedSe 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 positionEsta é 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.
ByteArrayInputStreameStringReaderjá atendemread()a partir de umbyte[]/Stringem memória; não há syscalls a amortizar. - Você está usando
Files.readString,Files.readAllBytes,Files.writeoutransferTo. Essas chamadas fazem seu próprio I/O em blocos com um grande buffer interno. Envolvê-las emBufferedInputStreamé 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.
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. transferTofoi 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.newBufferedReaderretornou umBufferedReaderdiretamente. Observe que nunca escrevemosnew 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 bytesantes deflush(). Esses caracteres estavam no buffer em memória, não em disco. Chamarflush()os empurrou para fora; sem o flush explícito (ou umclose()adequado), eles teriam se perdido. É por isso quetry-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 formawhile ((line = r.readLine()) != null): a atribuição-em-condição é idiomática aqui, e o sentinelnull(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.