Capítulo 12: Persistência e Manipulação de Arquivos: I/O Tradicional, Java NIO.2, Buffers e Streams
Especialização em Sistemas com Java 21 LTS • Módulo de Herança e Contratos
🗺️ Mapa Conceitual do Tópico
flowchart TD
subgraph LEGADO["java.io Clássico (Bloqueante / Prone to Bugs)"]
A["java.io.File (Retorna false sem motivo)"]
B["FileInputStream / FileOutputStream (Bytes Crus)"]
C["BufferedReader / BufferedWriter (Buffers em Charset)"]
A --> B & C
end
subgraph NIO2["Java NIO.2 (java.nio.file - Moderno & Robusto)"]
D["Path (Abstração Imutável de Caminhos)"]
E["Files (Operações Estáticas Atômicas)"]
F["Files.lines() (Streams Funcionais Lazy)"]
G["StandardCopyOption.ATOMIC_MOVE"]
D --> E
E --> F & G
end
subgraph DESEMPENHO["Estratégias de Manipulação de Dados"]
H["Leitura Pequena: Files.readString() / writeString()"]
I["Arquivos Gigantes (Giga/Tera): Files.lines() com Buffer"]
J["Binários / Mídia: Channels e ByteBuffers"]
end
NIO2 --> DESEMPENHO
style A fill:#ffebee,stroke:#f44336,stroke-width:2px
style D fill:#e1f5fe,stroke:#03a9f4,stroke-width:2px
style E fill:#ede7f6,stroke:#7e57c2,stroke-width:2px
style F fill:#e8f5e9,stroke:#4caf50,stroke-width:2px
style I fill:#fff3e0,stroke:#ff9800,stroke-width:2px
📁 1. A Evolução do I/O: De java.io.File para o Moderno NIO.2
O pacote clássico java.io.File foi desenhado no Java 1.0 e sofre de severas restrições arquiteturais:
- Métodos como
file.delete()oufile.createNewFile()retornam simples valores booleanos (false) em caso de erro, sem informar se a falha decorreu de permissões negadas, arquivo inexistente ou disco cheio. - Não possui suporte a atributos nativos de sistemas de arquivos modernos (permissões POSIX, links simbólicos, notificações assíncronas do kernel).
Com a introdução do Java NIO.2 (JSR-203) a partir do Java 7 e refinado até o Java 21:
java.nio.file.Path: Interface imutável e type-safe que representa um local no sistema de arquivos de qualquer sistema operacional.java.nio.file.Files: Classe utilitária recheada de operações atômicas que lançam exceções granulares e descritivas (NoSuchFileException,AccessDeniedException).
⚡ 2. Streams de Bytes vs Leitores de Caracteres (Char Streams)
A manipulação de dados em disco divide-se pelo tipo de dado transitado:
1. Streams de Bytes (InputStream e OutputStream)
Leem e escrevem bytes puros (0x00 a 0xFF). São obrigatórios para arquivos multimídia, planilhas binárias, executáveis ou arquivos criptografados.
try (InputStream is = Files.newInputStream(caminhoPdf)) { ... }
2. Leitores de Caracteres (Reader, Writer e Buffers)
Convertem bytes em caracteres aplicando tabelas de codificação (Charsets). No Java contemporâneo, sempre explicite o padrão StandardCharsets.UTF_8 para garantir portabilidade entre servidores Windows, Linux e macOS:
BufferedReader: Agrupa leituras em um buffer de memória (tamanho padrão de 8.192 caracteres), reduzindo drasticamente o número de system calls (interrupções no kernel do SO) para o disco.
🚀 3. Operações Modernas com Files no Java 11/17/21
A API moderna simplificou tarefas corriqueiras em poucas linhas de código elegante:
1. Leitura e Escrita Direta de Textos
Para arquivos de configuração ou payloads JSON/XML de pequeno ou médio porte:
Path caminho = Path.of("config", "app.json");
Files.writeString(caminho, jsonContent, StandardCharsets.UTF_8, StandardOpenOption.CREATE);
String conteudoLido = Files.readString(caminho, StandardCharsets.UTF_8);
2. Processamento Funcional Sob Demanda (Lazy Stream)
Para arquivos de log ou CSVs que ultrapassam a quantidade de memória RAM disponível na JVM:
// Abre o arquivo de forma sob demanda, fechando automaticamente com try-with-resources
try (Stream<String> linhas = Files.lines(Path.of("acessos.log"), StandardCharsets.UTF_8)) {
long erros500 = linhas.filter(l -> l.contains("HTTP 500")).count();
System.out.printf("Total de erros 500: %d%n", erros500);
}
📊 4. Matriz Comparativa: java.io.File vs java.nio.file.Path / Files
| Recurso | java.io.File (Legado) |
java.nio.file (NIO.2 Moderno) |
|---|---|---|
| Tratamento de Falhas | Retorna false silencioso |
Lança exceções ricas (IOException específicas) |
| Operações Atômicas | Não garantido | Sim (StandardCopyOption.ATOMIC_MOVE) |
| Links Simbólicos | Segue cegamente ou quebra | Configuração explícita (LinkOption.NOFOLLOW_LINKS) |
| Eficiência em Memória | Carrega listas inteiras em arrays | Streams preguiçosas (Files.lines(), Files.walk()) |
| Metadados do SO | Básico (tamanho, timestamp) | Acesso completo a atributos POSIX e ACLs |
💻 5. Código de Demonstração Corporativo (Java 21 LTS)
package com.empresa.arquivos;
import java.io.BufferedWriter;
import java.io.IOException;
import java.math.BigDecimal;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.util.stream.Stream;
/**
* Pipeline corporativo de processamento de arquivos CSV com Java NIO.2.
*/
public class ProcessadorArquivosNio {
public static void main(String[] args) {
System.out.println("================================================================================");
System.out.println(" PROCESSAMENTO EM LOTE COM JAVA NIO.2 (JAVA 21 LTS) ");
System.out.println("================================================================================");
Path diretorioTrabalho = Path.of("target", "lotes_financeiros");
Path arquivoOrigem = diretorioTrabalho.resolve("remessa_20260928.csv");
Path arquivoProcessado = diretorioTrabalho.resolve("remessa_processada.done");
try {
// 1. Criação atômica de diretórios se não existirem
Files.createDirectories(diretorioTrabalho);
// 2. Escrita simulada de arquivo de lote com BufferedWriter
try (BufferedWriter writer = Files.newBufferedWriter(arquivoOrigem, StandardCharsets.UTF_8,
StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)) {
writer.write("ID;CONTRATO;VALOR;STATUS\n");
writer.write("1;CONT-9021;14500.50;QUITADO\n");
writer.write("2;CONT-9022;3200.00;PENDENTE\n");
writer.write("3;CONT-9023;8900.25;QUITADO\n");
}
System.out.printf("📁 Arquivo de lote criado com sucesso: [%s]%n", arquivoOrigem);
// 3. Leitura preguiçosa com Files.lines() e cálculo com BigDecimal
BigDecimal totalQuitado = BigDecimal.ZERO;
int registrosProcessados = 0;
try (Stream<String> streamLinhas = Files.lines(arquivoOrigem, StandardCharsets.UTF_8)) {
for (String linha : (Iterable<String>) streamLinhas.skip(1)::iterator) {
String[] colunas = linha.split(";");
String status = colunas[3];
BigDecimal valor = new BigDecimal(colunas[2]);
if ("QUITADO".equalsIgnoreCase(status)) {
totalQuitado = totalQuitado.add(valor);
}
registrosProcessados++;
}
}
System.out.println("--------------------------------------------------------------------------------");
System.out.printf("Total de Registros Lidos : %d linhas%n", registrosProcessados);
System.out.printf("Volume Total Quitado : R$ %,10.2f%n", totalQuitado);
// 4. Renomeação atômica simulando conclusão de processamento
Files.move(arquivoOrigem, arquivoProcessado, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
System.out.printf("✅ Lote promovido para estado final: [%s]%n", arquivoProcessado.getFileName());
} catch (IOException e) {
System.err.printf("🚨 Falha na operação de I/O: %s%n", e.getMessage());
}
System.out.println("================================================================================");
}
}