1. Introdução
Vamos relembrar como funcionam FileReader e FileWriter. Toda vez que você chama o método read() ou write(), ocorre um acesso ao sistema de arquivos — ou seja, o computador realmente vai ao disco para ler ou gravar um único caractere. Se o arquivo for grande e você lê ou grava um caractere por vez, isso vira uma maratona de milhares ou até milhões de acessos ao disco. E o disco, como se sabe, não é o amigo mais rápido do programador.
Pense assim: você precisa transferir água de um balde para uma garrafa e faz isso com uma colher de chá. Formalmente — funciona, mas é terrivelmente demorado. Muito mais sensato é pegar uma concha ou uma caneca. Com arquivos é a mesma coisa: ler ou escrever um caractere por vez é como carregar água com uma colher.
É assim que acontece:
// Leitura do arquivo caractere a caractere (com "colher de chá"!)
try (FileReader reader = new FileReader("big.txt")) {
int c;
while ((c = reader.read()) != -1) {
// processamos o caractere (por exemplo, apenas contamos)
}
}
Se o arquivo for grande, você vai notar que o programa fica muito, muito lento.
Bufferização: o que é e por que é necessária
Buffer é uma área de memória especial (geralmente um array) em que os dados são carregados ou gravados não um caractere por vez, mas em blocos grandes (por exemplo, de 8 KB ou mais). Em outras palavras, um buffer é um pedaço de memória — algo como um balde intermediário — onde os dados são lidos/gravados em grandes porções, não caractere a caractere.
Funciona assim: na leitura, o programa acessa o disco uma vez, carrega um bloco inteiro de dados no buffer e depois os entrega a você calmamente, um caractere por vez, a partir da memória. Assim que o bloco termina, o próximo é carregado. Na escrita, a mesma lógica: os dados primeiro são acumulados no buffer e só depois enviados de uma vez ao disco (ou imediatamente, se você chamou flush()).
Por que isso é mais rápido? Porque o disco é lento por natureza, especialmente se você ficar acessando-o para coisinhas. Mas, se for a ele com menos frequência e pegar mais dados por vez, tudo funciona bem mais rápido. Em resumo: a bufferização economiza acessos ao disco e acelera o programa.
2. BufferedReader e BufferedWriter: sintaxe e exemplos
Em Java, para bufferizar a leitura e a escrita de arquivos de texto, usamos duas classes:
- BufferedReader — para leitura de arquivos de texto.
- BufferedWriter — para escrita de arquivos de texto.
Elas funcionam sobre os Reader/Writer comuns (por exemplo, FileReader/FileWriter), adicionando a bufferização.
Leitura do arquivo linha a linha com BufferedReader
O cenário mais comum é ler o arquivo por linhas. O método readLine() retorna a string até o caractere de nova linha ("\n" ou "\r\n").
import java.io.*;
public class BufferedReaderExample {
public static void main(String[] args) {
try (BufferedReader reader = new BufferedReader(new FileReader("input.txt"))) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line); // Imprime a linha na tela
}
} catch (IOException e) {
System.out.println("Erro ao ler o arquivo: " + e.getMessage());
}
}
}
O que está acontecendo aqui?
Criamos um FileReader, que sabe ler o arquivo caractere a caractere, e o envolvemos em um BufferedReader. O leitor bufferizado busca dados em blocos grandes, armazena-os na memória e nos entrega linha a linha por meio do método readLine(). No fim, você apenas escreve um laço while, recebe as linhas uma após a outra e não precisa se preocupar com o tamanho do arquivo: a leitura continuará rápida e econômica.
Gravando arquivo com BufferedWriter
Também é possível gravar linhas em arquivo de forma eficiente:
import java.io.*;
public class BufferedWriterExample {
public static void main(String[] args) {
try (BufferedWriter writer = new BufferedWriter(new FileWriter("output.txt"))) {
writer.write("Hello, world!");
writer.newLine(); // Quebra de linha (depende do SO)
writer.write("Esta é a segunda linha.");
} catch (IOException e) {
System.out.println("Erro ao gravar o arquivo: " + e.getMessage());
}
}
}
O que está acontecendo aqui?
Primeiro criamos um FileWriter e depois o envolvemos em um BufferedWriter. Quando você chama write() ou newLine(), os dados não vão direto ao disco. Eles são colocados no buffer — uma camada de memória intermediária. Somente quando o buffer enche ou quando você fecha o fluxo (ou chama explicitamente flush()), todo o texto acumulado é gravado de uma vez no arquivo. Essa abordagem acelera bastante a escrita e economiza acessos ao disco.
Como isso fica em um aplicativo único?
Suponha que estamos escrevendo um “diário” simples, que salva entradas em um arquivo e as exibe na tela.
import java.io.*;
import java.util.Scanner;
public class DiaryApp {
public static void main(String[] args) {
String fileName = "diary.txt";
Scanner scanner = new Scanner(System.in);
// Gravar nova entrada
System.out.print("Digite uma nova entrada no diário: ");
String entry = scanner.nextLine();
try (BufferedWriter writer = new BufferedWriter(new FileWriter(fileName, true))) {
writer.write(entry);
writer.newLine();
System.out.println("Entrada salva!");
} catch (IOException e) {
System.out.println("Erro ao gravar: " + e.getMessage());
}
// Leitura de todas as entradas
System.out.println("\nSeu diário:");
try (BufferedReader reader = new BufferedReader(new FileReader(fileName))) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
System.out.println("Erro ao ler: " + e.getMessage());
}
}
}
Pegamos o nome do arquivo "diary.txt" e pedimos ao usuário que digite uma nova entrada. Para salvar, usamos FileWriter no modo append (passamos true), assim as anotações antigas não são apagadas — cada nova linha é adicionada cuidadosamente ao final do arquivo. O “envoltório” BufferedWriter torna a escrita rápida e econômica: os dados primeiro se acumulam na memória e depois são gravados de uma vez no disco.
Depois disso, abrimos o mesmo arquivo para leitura com BufferedReader. Ele busca o conteúdo em blocos grandes e entrega as linhas uma a uma. No laço, o programa simplesmente as imprime na tela e, no fim, você vê todo o seu “diário” do começo ao fim.
3. Vantagens de BufferedReader e BufferedWriter
Aceleração significativa
Ao ler ou escrever arquivo com fluxos bufferizados, o programa faz dezenas e, às vezes, centenas de vezes menos acessos ao disco. Isso é especialmente perceptível em arquivos grandes.
Métodos convenientes
- BufferedReader.readLine() — permite ler o arquivo por linhas, o que é muito conveniente para processar arquivos de texto (por exemplo, logs, CSV, configs).
- BufferedWriter.newLine() — adiciona uma quebra de linha, inserindo corretamente o caractere adequado conforme o SO.
Facilidade de uso
- As classes se combinam facilmente com outros fluxos (por exemplo, é possível envolver InputStreamReader dentro de BufferedReader para ler arquivos com diferentes codificações).
- Fecham automaticamente todos os recursos quando usadas com try-with-resources.
Flexibilidade
- É possível definir explicitamente o tamanho do buffer, caso o padrão (normalmente 8 KB) seja pequeno ou grande demais para você:
BufferedReader reader = new BufferedReader(new FileReader("big.txt"), 16384); // buffer de 16 KB
4. Quando usar BufferedReader e BufferedWriter
Use-os:
- Se estiver trabalhando com arquivos de texto (logs, CSV, grandes volumes de texto).
- Quando precisar ler ou escrever o arquivo linha a linha.
- Para acelerar o trabalho com arquivos grandes, quando a velocidade for importante.
- Se precisar processar um fluxo de dados de rede ou outra fonte que suporte Reader/Writer.
Não os use:
- Para trabalhar com arquivos binários (por exemplo, imagens, arquivos compactados, vídeos) — para isso existem InputStream/OutputStream.
- Se o arquivo for muito pequeno e for lido/gravado uma única vez por completo — aqui o ganho com a bufferização será mínimo (mas também não fará mal).
5. Nuances úteis
Combinação com diferentes codificações
Se você precisa ler/gravar arquivos em uma codificação específica (por exemplo, "UTF-8", "Windows-1251"), use InputStreamReader/OutputStreamWriter em conjunto com fluxos bufferizados:
BufferedReader reader = new BufferedReader(
new InputStreamReader(new FileInputStream("input.txt"), "UTF-8")
);
BufferedWriter writer = new BufferedWriter(
new OutputStreamWriter(new FileOutputStream("output.txt"), "UTF-8")
);
Descarregamento explícito do buffer
Às vezes é importante garantir que os dados cheguem ao disco imediatamente (por exemplo, se você estiver escrevendo um log ou um recibo). Para isso, chame writer.flush(). Normalmente isso não é necessário, pois fechar o fluxo já descarrega o buffer automaticamente.
Tamanho do buffer
Por padrão, o buffer tem cerca de 8 KB. É possível definir outro tamanho se você souber que isso trará ganho de desempenho (por exemplo, ao processar arquivos gigantes).
Comparação: FileReader/FileWriter vs BufferedReader/BufferedWriter
| Classe | Velocidade em arquivos grandes | Leitura linha a linha | Gravação linha a linha | Flexibilidade com codificações |
|---|---|---|---|---|
| FileReader/FileWriter | Lento | Não (apenas caractere a caractere) | Não (apenas caractere a caractere) | Apenas padrão |
| BufferedReader/Writer | Rápido | Sim (readLine()) | Sim (newLine()) | Sim (via InputStreamReader/OutputStreamWriter) |
6. Erros comuns ao trabalhar com BufferedReader e BufferedWriter
Erro nº 1: Esqueceu de fechar o fluxo. Se você não usar try-with-resources ou não chamar close(), o arquivo pode permanecer bloqueado e os dados podem não ser gravados no disco. Use sempre try-with-resources!
Erro nº 2: Confundir trabalho com arquivos de texto e binários. Tentar abrir um arquivo binário (".jpg", ".zip") com BufferedReader levará a “caracteres ilegíveis” e, muito provavelmente, a erros. Para arquivos binários, use InputStream/OutputStream.
Erro nº 3: Não usar bufferização com grandes volumes de dados. Se você ler ou escrever caractere a caractere, o programa será lento. Use sempre bufferização para arquivos grandes.
Erro nº 4: Não chamar flush() quando necessário. Se for preciso que os dados apareçam no disco imediatamente (por exemplo, para logging), chame writer.flush(). Mas, em geral, fechar o fluxo já resolve.
Erro nº 5: Não levar a codificação em conta. Se você abrir um arquivo com a codificação errada, o texto pode aparecer incorretamente (caracteres viram “?”, ideogramas etc.). Sempre indique explicitamente a codificação necessária, se ela for diferente da do sistema (por exemplo, "UTF-8").
GO TO FULL VERSION