Streams de Buffer em Java: BufferedInputStream e BufferedOutputStream
Operações de entrada e saída (I/O), especialmente com arquivos e rede, são inerentemente lentas em comparação com o acesso à memória RAM. Essa lentidão se deve à latência dos dispositivos físicos (como discos rígidos) e ao custo das chamadas de sistema que o programa precisa fazer ao sistema operacional para cada operação de leitura ou escrita.
Para mitigar esse impacto na performance, Java oferece streams de buffer. Em vez de ler ou escrever um byte de cada vez no dispositivo, eles operam em grandes blocos de dados, utilizando um buffer (uma área de armazenamento temporário) na memória. Isso minimiza drasticamente o número de interações com o dispositivo físico, resultando em um ganho de velocidade significativo.
Essa abordagem, onde um stream "envolve" outro para adicionar uma nova funcionalidade, é uma implementação do padrão de projeto Decorator.
A Classe BufferedInputStream
A classe BufferedInputStream
adiciona um buffer a um stream de entrada existente. Quando o código solicita a leitura de dados, o BufferedInputStream
preenche seu buffer interno lendo um grande bloco de dados do stream subjacente de uma só vez. As solicitações de leitura subsequentes são então atendidas a partir desse buffer rápido em memória. Somente quando o buffer fica vazio, outra leitura em bloco é realizada.
BufferedInputStream
possui dois construtores:
BufferedInputStream(InputStream inputStream)
BufferedInputStream(InputStream inputStream, int bufSize)
O primeiro parâmetro é o stream de entrada a ser "decorado". O segundo, opcional, permite especificar o tamanho do buffer em bytes.
No exemplo a seguir, a leitura de um ByteArrayInputStream
é otimizada com um buffer:
import java.io.*;
public class Program {
public static void main(String[] args) {
String text = "Hello world!";
byte[] buffer = text.getBytes();
ByteArrayInputStream in = new ByteArrayInputStream(buffer);
// Envolve o stream de bytes em um buffer para otimizar a leitura
try (BufferedInputStream bis = new BufferedInputStream(in)) {
int c;
while ((c = bis.read()) != -1) {
System.out.print((char) c);
}
} catch (Exception e) {
System.out.println(e.getMessage());
}
System.out.println();
}
}
Embora este exemplo funcione, o ganho de performance é exponencial ao trabalhar com fontes de dados lentas, como FileInputStream
(arquivos em disco) ou streams de rede. Nesses cenários, reduzir as leituras físicas de, por exemplo, mil para apenas uma, pode mudar drasticamente a velocidade da aplicação.
A Classe BufferedOutputStream
De forma análoga, a BufferedOutputStream
adiciona um buffer a um stream de saída. Os dados enviados ao método write()
são acumulados no buffer em memória em vez de serem enviados imediatamente ao destino. A escrita real no stream subjacente só ocorre quando o buffer está cheio, ou quando o método flush()
é chamado explicitamente, ou — mais comumente — quando o stream é fechado com close()
.
BufferedOutputStream
também oferece dois construtores:
BufferedOutputStream(OutputStream outputStream)
BufferedOutputStream(OutputStream outputStream, int bufSize)
O exemplo a seguir demonstra a escrita em um arquivo com o uso de um buffer:
import java.io.*;
public class Program {
public static void main(String[] args) {
String text = "Hello world!";
try (FileOutputStream out = new FileOutputStream("notes.txt");
// Envolve o stream de arquivo em um buffer para otimizar a escrita
BufferedOutputStream bos = new BufferedOutputStream(out)) {
byte[] buffer = text.getBytes();
bos.write(buffer, 0, buffer.length);
} catch (IOException ex) {
System.out.println(ex.getMessage());
}
}
}
Quando bos.write()
é chamado, os dados são gravados rapidamente no buffer em memória. A estrutura try-with-resources
garante que, ao final do bloco, o método close()
seja chamado no BufferedOutputStream
. O método close()
, por sua vez, força o flush()
final dos dados restantes no buffer para o arquivo em disco. Sem o close()
ou flush()
, os últimos dados poderiam nunca ser gravados no arquivo de destino.
Resumo
- Streams de buffer (
BufferedInputStream
,BufferedOutputStream
) melhoram drasticamente o desempenho de I/O ao reduzir a latência. - Eles minimizam o número de chamadas ao sistema operacional, lendo e escrevendo grandes blocos de dados em memória.
- Operam como Decorators, envolvendo um stream existente para adicionar a funcionalidade de buffering sem alterar a interface original.
- É crucial fechar o
BufferedOutputStream
(ou usartry-with-resources
) para garantir que todos os dados no buffer sejam efetivamente gravados no destino (flush
).