Leitura de Arquivos em Java
Leia arquivos de texto e binários em Java com FileReader, BufferedReader, Scanner, Files.readString e streams.
Existem cinco formas comuns de ler um arquivo de texto em Java, e a escolha certa depende quase inteiramente do tamanho do arquivo e do que você deseja fazer com o conteúdo. Este capítulo apresenta as cinco opções, da mais simples à mais flexível:
Files.readString(path)— arquivo inteiro como umaString.Files.readAllLines(path)— arquivo inteiro como umaList<String>.Files.readAllBytes(path)— arquivo inteiro como umbyte[].Files.lines(path)— arquivo como umStream<String>, lazy.BufferedReader/Scanner— decoradores clássicos, controle total.
Escolha a menor ferramenta que atenda à necessidade. Ler um log de 4 GB com Files.readString resulta em OutOfMemoryError; ler uma configuração de 12 linhas com BufferedReader e um loop while são seis linhas de código onde uma bastaria.
Files.readString(path) — arquivo inteiro, uma chamada
String text = Files.readString(Path.of("config.json"), StandardCharsets.UTF_8);Adicionado no Java 11. Retorna o arquivo completo como uma String. Usa UTF-8 por padrão desde o Java 18 (Charset ainda é fortemente recomendado para fixar explicitamente, mesmo com o novo padrão). Lança IOException se o arquivo não existir ou não puder ser lido; lança OutOfMemoryError se o arquivo for maior do que o heap.
Use quando: o arquivo é "pequeno o suficiente" — arquivos de configuração, payloads JSON, capítulos MDX, qualquer coisa que você esteja disposto a ler em uma única janela do editor. A regra informal clássica é abaixo de alguns megabytes.
Files.readAllLines(path) — lista de linhas
List<String> lines = Files.readAllLines(Path.of("hosts.txt"), StandardCharsets.UTF_8);Retorna uma List<String> imutável com as linhas do arquivo, com os terminadores de linha removidos. Mesmo perfil de memória que readString mais o overhead da List — também mantém o arquivo inteiro na memória.
Use quando: você quer indexar por número de linha, ordenar o arquivo ou alimentar linhas em um loop for (String line : lines) sem configurar streams.
Files.readAllBytes(path) — bytes brutos
byte[] raw = Files.readAllBytes(Path.of("photo.png"));O equivalente em bytes. Sem Charset porque não há decodificação. Use para arquivos binários (imagens, arquivos compactados, executáveis) ou quando precisar calcular um hash ou redirecionar bytes para um ByteArrayInputStream.
Files.lines(path) — stream lazy
try (Stream<String> lines = Files.lines(Path.of("app.log"), StandardCharsets.UTF_8)) {
long errors = lines.filter(l -> l.contains("ERROR")).count();
}Este é o único leitor nativo que escala para arquivos arbitrariamente grandes. O Stream<String> é lazy — as linhas são lidas sob demanda, não todas de uma vez — e se conecta diretamente ao vocabulário de pipeline de stream (filter, map, count, toList).
Dois pontos inegociáveis:
try-with-resources é obrigatório. O stream possui um identificador de arquivo aberto; semtry-with-resources, o arquivo permanece aberto até o GC, e você esgotará os descritores de arquivo em um servidor ocupado.- Não reutilize o stream após uma operação terminal. Streams são de uso único.
Use quando: o arquivo é grande demais para readAllLines, ou você quer que a transformação linha por linha se componha com o restante do seu pipeline de stream.
BufferedReader.readLine() — o clássico
BufferedReader é o motor de trabalho que os helpers modernos encapsulam. Ele armazena em buffer as leituras subjacentes em um bloco de tamanho fixo na memória, de modo que readLine() não emite uma syscall por caractere.
try (BufferedReader in = Files.newBufferedReader(Path.of("hosts.txt"), StandardCharsets.UTF_8)) {
String line;
while ((line = in.readLine()) != null) {
System.out.println(line);
}
}Files.newBufferedReader(path) é a factory moderna; a versão clássica é new BufferedReader(new FileReader("hosts.txt")) (que usa o charset da plataforma em JDKs anteriores ao 18 — fixe com UTF-8 usando a sobrecarga de três argumentos). O contrato de readLine() é:
- Retorna a próxima linha sem seu terminador (
\n,\rou\r\n). - Retorna
nullno final do arquivo. A condição de loop(line = readLine()) != nullé o idioma estabelecido.
BufferedReader também é um produtor de Stream<String>: reader.lines() retorna um Stream<String> respaldado pelo reader. É assim que Files.lines é implementado internamente.
Scanner — análise token por token
Scanner lê texto por tokens — palavras, inteiros, doubles, linhas, até correspondências de regex — e é a ferramenta certa para ler entrada estruturada onde as unidades não são linhas inteiras.
try (Scanner sc = new Scanner(Files.newBufferedReader(Path.of("nums.txt")))) {
while (sc.hasNextInt()) {
int n = sc.nextInt();
System.out.println(n * n);
}
}Scanner é mais lento que BufferedReader porque faz análise; aloca strings curtas e executa regex. Para processamento linha por linha, prefira BufferedReader. Para tokens tipados de um arquivo pequeno (números, palavras, entrada no estilo CSV), Scanner elimina a camada de análise.
Há um capítulo completo sobre Scanner mais adiante nesta parte — esta é a variante de leitura de arquivo.
FileReader — o leitor de caracteres bruto
try (FileReader in = new FileReader("notes.txt", StandardCharsets.UTF_8)) {
int c;
while ((c = in.read()) != -1) {
System.out.print((char) c);
}
}FileReader lê caracteres diretamente do arquivo — sem buffering, sem reconhecimento de linha, sem escolhas de decodificação feitas por você (você passa o Charset, ou aceita o padrão da plataforma em JDKs anteriores ao 18). É a camada sobre a qual os outros se apoiam. Você quase nunca o usa diretamente no código de aplicação; você o envolve em um BufferedReader.
Ainda é útil quando você quer ler algumas centenas de caracteres e parar — buscas pequenas onde o custo da configuração de um buffer é eclipsado pelo custo da chamada.
Qual usar
| Cenário | Escolha |
|---|---|
Arquivo pequeno que você quer como uma única String | Files.readString |
Arquivo pequeno que você quer como uma List<String> | Files.readAllLines |
| Arquivo binário (imagem, arquivo compactado) | Files.readAllBytes |
| Qualquer arquivo com transformação no estilo stream | Files.lines (dentro de try-with-resources) |
| Loop linha por linha, controle total | Files.newBufferedReader + readLine |
| Tokens tipados (ints, palavras, correspondências de regex) | Scanner |
| Um caractere por vez, arquivo pequeno | FileReader |
O padrão correto para o caso "só quero carregar este arquivo de texto pequeno" é Files.readString. O padrão correto para "processar este log gigante sem estourar a memória" é Files.lines.
Um exemplo prático: mesmo arquivo, cinco leitores
O programa abaixo escreve um pequeno arquivo de texto e depois o lê de cinco maneiras diferentes — readString, readAllLines, Files.lines filtrado por um Predicate<String> do vocabulário da Parte 12, BufferedReader.readLine e Scanner para inteiros tokenizados. Cada bloco imprime o que obteve para que você possa ver os formatos lado a lado.
O que aprender com a execução:
Files.readStringretornou o arquivo inteiro como umaString— fácil e exatamente o que você quer para configurações e templates pequenos. Para um log de 4 GB, teria lançadoOutOfMemoryError.Files.readAllLinesretornou umaList<String>indexável com os terminadores removidos.lines.get(0)funcionou porque a lista está materializada na memória; você não conseguiria fazer isso com um stream.Files.lines(file)foi aberto dentro detry-with-resources porque o stream possui o identificador de arquivo. O pipeline.filter(isError).count()tem a mesma forma que qualquer coisa da Parte 12 — apenas a fonte mudou.BufferedReader.readLine()retornounullno final do arquivo. O loopforaqui parou em três de propósito, mas o idioma de produção éwhile ((line = in.readLine()) != null).Scannerpulou linhas que não começavam com um inteiro, depois leu tokens comnextInt()até acabarem. O mesmoScannerpoderia ter lido doubles (nextDouble), correspondências de regex (findInLine), ouBigIntegers — é por isso que custa mais por token do queBufferedReadercusta por linha.
O que vem a seguir
O próximo capítulo, Escrita de Arquivos em Java, aborda o lado de escrita das mesmas APIs — Files.writeString, Files.write, BufferedWriter, PrintWriter e as flags StandardOpenOption (APPEND, CREATE_NEW, TRUNCATE_EXISTING) que decidem como um arquivo existente é tratado.