Introdução ao Java I/O
Visão geral do Java I/O: streams de bytes vs. caracteres, I/O com buffer, java.io vs. java.nio.file.
A Parte 12 terminou com um vocabulário que você pode levar diretamente para esta parte: lambdas, Consumer<T> e Supplier<T> como os formatos por trás de "me dê uma linha" e "faça algo com essa linha," try-with-resources para qualquer coisa que precise de limpeza determinística, e o pipeline Stream para dados orientados a linhas. As APIs de I/O do Java foram projetadas exatamente em torno dessas formas — muito antes de existirem as palavras "interface funcional," os objetos subjacentes já tinham cada um apenas um método, e a fachada pós-Java-8 trouxe o restante do caminho.
Esta parte cobre quatro conjuntos de ferramentas sobrepostos:
- Streams
java.io— a API original do Java 1.0:InputStream/OutputStreampara bytes,Reader/Writerpara caracteres, e os decoradoresBuffered*,Data*,Print*que os envolvem. java.io.File— a classe legada "esta string é um caminho". Ainda presente em código mais antigo; substituída porjava.nio.file.Pathpara novos projetos.java.nio.file— a API moderna (Java 7+):Path,Filese os auxiliares estáticos (Files.readString,Files.writeString,Files.lines,Files.walk) que transformam a maioria das operações de arquivo em uma única linha.- Serialização — transformar grafos de objetos em bytes e vice-versa com
ObjectOutputStream/ObjectInputStream.
Os primeiros seis capítulos percorrem as operações de arquivo de alto nível (abrir, criar, ler, escrever, excluir) usando tanto java.io quanto java.nio.file para que você possa ver a mesma tarefa de duas formas. Os capítulos intermediários focam nas próprias classes de stream — byte vs. caractere, buffer, dados, impressão. Os últimos capítulos cobrem serialização e a API de path/walk.
A divisão byte/caractere
Toda API de I/O em java.io tem uma de duas formas:
InputStream / OutputStream — byte-oriented (raw bytes: int read() returns 0..255 or -1)
Reader / Writer — character-oriented (decoded text: int read() returns a char or -1)A divisão não é cosmética. Bytes são o que discos e sockets armazenam; caracteres são o que os humanos leem. Um .png é bytes; um .txt também é bytes no disco, mas normalmente você quer que seja decodificado em caracteres usando um charset. Misturar os dois sem um charset é a fonte mais comum de bugs de "caracteres estranhos" no Java legado.
As classes de ponte — InputStreamReader e OutputStreamWriter — convertem entre os dois e recebem um argumento Charset. Use StandardCharsets.UTF_8 a menos que tenha um motivo documentado para usar outra coisa; as formas sem argumento usam o padrão da plataforma, que difere entre sistemas operacionais e é a fonte clássica de bugs do tipo "funciona no meu Mac, quebra no servidor Linux."
O padrão decorator
java.io é construído sobre o padrão decorator: um pequeno conjunto de streams brutos (FileInputStream, FileOutputStream, FileReader, FileWriter) envolvidos em funcionalidades em camadas (buffer, texto linha a linha, tipos primitivos, saída formatada). Você compõe o que precisa no ponto de chamada:
// Read a UTF-8 text file line by line:
try (BufferedReader in = new BufferedReader(
new InputStreamReader(new FileInputStream("a.txt"), StandardCharsets.UTF_8))) {
String line;
while ((line = in.readLine()) != null) {
System.out.println(line);
}
}Três camadas, de baixo para cima: FileInputStream lê bytes brutos; InputStreamReader os decodifica como caracteres UTF-8; BufferedReader adiciona um buffer em memória e o método readLine(). Cada camada é uma classe separada com uma única responsabilidade. O Java 11 simplificou exatamente esse padrão para uma linha — Files.newBufferedReader(path) — mas a decoração ainda é o que acontece por baixo.
try-with-resources é a regra
Todo stream, reader, writer e channel em java.io e java.nio implementa AutoCloseable. Fechar importa: um FileOutputStream não fechado pode perder seu buffer final; um socket não fechado vaza um file descriptor; um reader não fechado no Windows mantém um bloqueio que o SO não libera. O construtor try-with-resources (Java 7+) garante que close() seja chamado em todos os caminhos, com sucesso ou exceção:
try (BufferedReader in = Files.newBufferedReader(path)) {
return in.readLine();
} // close() runs here, even if readLine() throwsVocê pode declarar mais de um recurso no mesmo try; eles são fechados em ordem inversa. Código antigo com try/finally que chama close() manualmente está errado em quase todos os casos — a exceção interna engole a exceção do fechamento, ou o próprio fechamento é esquecido no caminho de erro. Use try-with-resources para qualquer coisa que abra um handle.
java.io versus java.nio.file
java.io.File (1996) modelava um caminho como uma String e oferecia um punhado de operações (exists, isFile, delete, listFiles). A classe ainda está em todo lugar no código legado, e muitas APIs ainda retornam ou aceitam File. Mas ela tem limitações que o JDK não mais dissimula:
- Nenhuma forma de perguntar por que uma operação falhou —
file.delete()retornafalsetanto para "o arquivo não existe," "permissão negada," quanto para "o arquivo está aberto." Você não consegue distinguir. - Nenhum suporte para links simbólicos, atributos de arquivo, permissões ou operações atômicas.
- Nenhuma forma de percorrer uma árvore de diretórios sem escrever a recursão você mesmo.
java.nio.file (Java 7) o substitui. Path é o novo tipo "isso é um caminho," e Files é uma classe utilitária static com cerca de 80 métodos para tudo que você queira fazer com ele:
Path p = Path.of("data", "users.txt"); // platform-independent path
String text = Files.readString(p, StandardCharsets.UTF_8); // whole file, one call
List<String> lines = Files.readAllLines(p, StandardCharsets.UTF_8);
Files.writeString(p, "hello\n", StandardCharsets.UTF_8);
try (Stream<String> s = Files.lines(p, StandardCharsets.UTF_8)) {
s.filter(l -> !l.isBlank()).forEach(System.out::println);
}Dois pontos a observar. Primeiro, Files.lines(path) retorna um Stream<String> — o pipeline de stream que você aprendeu na Parte 12 lê arquivos diretamente. Segundo, o stream possui um file handle aberto, portanto o wrapper try-with-resources é obrigatório — sem ele, o arquivo permanece aberto até o próximo GC.
Ao longo da Parte 13 mostraremos ambas as APIs lado a lado. Código novo deve preferir java.nio.file; os capítulos legados existem porque você encontrará as formas mais antigas em qualquer base de código anterior ao Java 11.
Para onde esta parte vai
- O próximo capítulo, Java File Class, percorre a API legada
java.io.File— seus métodos de consulta, listagem e as limitações que motivaram ojava.nio.file. - Os quatro capítulos seguintes (Creating Files, Reading Files, Writing Files, Deleting Files) cobrem as operações de alto nível "fazer uma coisa com um arquivo" usando ambas as APIs.
- Capítulos sobre streams de byte, caractere e com buffer focam então na pilha de decoradores
java.iosubjacente. - Serialização, depois
Path,Filese a API de percurso de diretórios encerram a parte.
Um exemplo prático: a mesma tarefa de quatro formas
O programa abaixo escreve um arquivo de texto curto de quatro formas — uma vez com o moderno Files.writeString, uma vez com o clássico FileWriter + try-with-resources, uma vez decorado com BufferedWriter, e uma vez com PrintWriter para saída formatada. Em seguida, lê o arquivo de volta duas vezes — uma com Files.readString (arquivo inteiro, uma chamada) e uma com Files.lines como um Stream<String> filtrado com um Predicate<String>. O exemplo usa um arquivo temporário do sistema para funcionar em qualquer sandbox.
O que tirar da execução:
- O mesmo arquivo foi escrito de quatro formas diferentes.
Files.writeStringé o caminho mais curto para "coloque esta string neste arquivo";FileWriteré o writer bruto clássico;BufferedWriteradiciona um buffer em memória (econômico quando você escreve muitos pequenos fragmentos);PrintWriteradicionaprintf. Cada um sobrescreveu o conteúdo anterior porque o modo de abertura padrão é "truncar e escrever" — você precisa passarStandardOpenOption.APPEND(coberto no capítulo de escrita) para adicionar a um arquivo. - Todo writer foi executado dentro de
try-with-resources. Omitir isso em um writer com buffer é o bug em que os últimos caracteres nunca chegam ao disco —close()é o que descarrega o buffer final. Files.readStringretornou o arquivo inteiro como uma únicaString— adequado para arquivos pequenos, a escolha errada para um log de 4 GB.Files.linesretornou umStream<String>que você pode encadear comfilter,mapecountsem manter o arquivo inteiro na memória. Otry-with-resources obrigatório no stream existe porque o stream possui um file handle aberto.- A linha
Predicate<String> nameLine = l -> l.startsWith("name")usa o mesmo vocabulário da Parte 12 — um valorPredicate, passado paraStream.filter.Files.linesé onde a API de streams encontra a API de I/O. Files.deleteIfExistsé a exclusão sem exceção: retornatruese removeu o arquivo,falsese ele não existia. O legadoFile.delete()retorna umbooleantanto para "excluído" quanto para "não conseguiu excluir" —Filesdistingue os dois lançando exceção.
O que vem a seguir
Antes que a API moderna java.nio.file domine o restante da parte, o próximo capítulo cobre a classe que você encontrará primeiro em qualquer base de código mais antiga: Java File Class. É o tipo legado de caminho e metadados — limitado, mas onipresente — e ver o que ele não consegue fazer é o que justifica o uso de Path e Files.