CodeGym /Cursos /JAVA 25 SELF /BufferedReader, BufferedWriter: bufferização, vantagens

BufferedReader, BufferedWriter: bufferização, vantagens

JAVA 25 SELF
Nível 36 , Lição 0
Disponível

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").

Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION