W3docs

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:

  1. Streams java.io — a API original do Java 1.0: InputStream/OutputStream para bytes, Reader/Writer para caracteres, e os decoradores Buffered*, Data*, Print* que os envolvem.
  2. java.io.File — a classe legada "esta string é um caminho". Ainda presente em código mais antigo; substituída por java.nio.file.Path para novos projetos.
  3. java.nio.file — a API moderna (Java 7+): Path, Files e 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.
  4. 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() throws

Você 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() retorna false tanto 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 o java.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.io subjacente.
  • Serialização, depois Path, Files e 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.

java— editable, runs on the server

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; BufferedWriter adiciona um buffer em memória (econômico quando você escreve muitos pequenos fragmentos); PrintWriter adiciona printf. Cada um sobrescreveu o conteúdo anterior porque o modo de abertura padrão é "truncar e escrever" — você precisa passar StandardOpenOption.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.readString retornou o arquivo inteiro como uma única String — adequado para arquivos pequenos, a escolha errada para um log de 4 GB. Files.lines retornou um Stream<String> que você pode encadear com filter, map e count sem manter o arquivo inteiro na memória. O try-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 valor Predicate, passado para Stream.filter. Files.lines é onde a API de streams encontra a API de I/O.
  • Files.deleteIfExists é a exclusão sem exceção: retorna true se removeu o arquivo, false se ele não existia. O legado File.delete() retorna um boolean tanto para "excluído" quanto para "não conseguiu excluir" — Files distingue 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.

Prática

Prática
Por que o `java.io` separa `InputStream`/`OutputStream` de `Reader`/`Writer`?
Por que o `java.io` separa `InputStream`/`OutputStream` de `Reader`/`Writer`?
Was this page helpful?