Classe Files do Java NIO
Operações de alto nível no sistema de arquivos em Java com java.nio.file.Files — ler, escrever, copiar, mover e percorrer.
Path (o capítulo anterior) era o substantivo. Files é o verbo — uma classe utilitária estática cujo cada método recebe um Path e faz algo com o arquivo naquele caminho. É o lar dos one-liners que têm silenciosamente tornado o resto desta parte mais curta: Files.readString, Files.newBufferedReader, Files.createTempFile, Files.size. Este capítulo percorre o catálogo completo.
Files é grande — cerca de 80 métodos — e agrupado por finalidade: ler, escrever, criar, inspecionar, modificar, percorrer. Você não precisa memorizá-lo; precisa saber que é o primeiro lugar a procurar quando quiser fazer qualquer coisa com um arquivo.
Leitura
Os leitores de arquivo inteiro são uma linha cada:
String text = Files.readString(path); // UTF-8 by default (Java 11+)
String utf16 = Files.readString(path, StandardCharsets.UTF_16);
byte[] bytes = Files.readAllBytes(path);
List<String> lines = Files.readAllLines(path, StandardCharsets.UTF_8);Para arquivos pequenos o suficiente para caber na memória, readString e readAllBytes são as ferramentas certas. Eles abrem o arquivo, leem tudo, fecham e entregam o conteúdo. Sem streams, sem buffers, sem lógica de fechamento.
Para arquivos grandes demais para carregar por completo, use as formas de streaming:
try (BufferedReader r = Files.newBufferedReader(path, StandardCharsets.UTF_8)) {
String line;
while ((line = r.readLine()) != null) process(line);
}
try (Stream<String> lines = Files.lines(path, StandardCharsets.UTF_8)) {
lines.filter(...).forEach(...); // closes the file when the stream closes
}
try (InputStream in = Files.newInputStream(path)) {
// raw bytes for binary formats
}Files.lines é BufferedReader.lines com a tubulação de abertura e fechamento embutida. O try-with-resources em torno do Stream faz o fechamento — sem ele, o handle do arquivo vaza.
Escrita
Mesma forma do lado da escrita:
Files.writeString(path, "hello\n", StandardCharsets.UTF_8);
Files.write(path, bytes); // byte[]
Files.write(path, lines, StandardCharsets.UTF_8); // Iterable<? extends CharSequence>Os três são atômicos em uma chamada: abrir, escrever, fechar. Por padrão eles criam ou truncam — se o arquivo existia, seu conteúdo anterior é apagado. Para acrescentar:
Files.writeString(path, "more\n", StandardCharsets.UTF_8, StandardOpenOption.APPEND);Para a forma de streaming (escrita incremental):
try (BufferedWriter w = Files.newBufferedWriter(path, StandardCharsets.UTF_8)) {
for (String line : lines) w.write(line);
}Opções de abertura
Todo método de leitura/escrita que abre um arquivo aceita um varargs opcional de StandardOpenOption:
| Opção | Significado |
|---|---|
READ | Abrir para leitura |
WRITE | Abrir para escrita |
CREATE | Criar se ausente; não fazer nada se presente |
CREATE_NEW | Criar se ausente; falhar se presente |
APPEND | Escritas vão para o final do arquivo |
TRUNCATE_EXISTING | Limpar conteúdo ao abrir |
DELETE_ON_CLOSE | Excluir quando o canal fechar (arquivos temporários) |
SYNC / DSYNC | Bloquear escritas até o SO confirmar que os dados estão no disco |
O modo de abertura padrão para newBufferedWriter e writeString é CREATE, TRUNCATE_EXISTING, WRITE. O padrão para newBufferedReader e readString é READ. Opções explícitas substituem os padrões — passar qualquer opção desliga o conjunto implícito, então você normalmente precisa repetir as implícitas ao personalizar:
Files.newBufferedWriter(path, StandardCharsets.UTF_8,
StandardOpenOption.CREATE,
StandardOpenOption.APPEND); // appends, creates if absentCriação
Files.createFile(path); // empty file; fails if it exists
Files.createDirectory(path); // single dir; fails if parent absent
Files.createDirectories(path); // recursive: like `mkdir -p`
Files.createSymbolicLink(link, target);
Files.createLink(link, target); // hard link
Path tmpFile = Files.createTempFile("prefix-", ".txt"); // in the default temp dir
Path tmpDir = Files.createTempDirectory("prefix-");createDirectories é a ferramenta certa para "quero que este diretório exista." É idempotente: se o diretório já está lá, retorna sem erro; se algum ancestral está faltando, cria toda a cadeia. createDirectory (sem -ies) faz apenas um nível e falha se o pai não existir — quase sempre errado, a menos que você precise especificamente dessa verificação.
Para arquivos temporários, as sobrecargas de createTempFile e createTempDirectory escolhem o diretório temporário do sistema automaticamente e retornam o Path criado. Combine-os com .toFile().deleteOnExit() para limpeza, ou use Files.delete explícito em um finally.
Inspeção
Os predicados e acessores:
boolean ok = Files.exists(path);
boolean nope = Files.notExists(path); // NOT the negation of exists
boolean file = Files.isRegularFile(path);
boolean dir = Files.isDirectory(path);
boolean link = Files.isSymbolicLink(path);
boolean read = Files.isReadable(path);
boolean write = Files.isWritable(path);
boolean exec = Files.isExecutable(path);
long size = Files.size(path); // throws IOException
FileTime mtime = Files.getLastModifiedTime(path);
String mimeType = Files.probeContentType(path); // best-effort, can return null
UserPrincipal owner = Files.getOwner(path);exists e notExists não são negações: ambos podem retornar false quando o acesso ao arquivo não pode ser determinado (permissão negada, symlink pendente). Use o correto para o que você quer — !exists(p) e notExists(p) diferem em casos extremos.
Copiar, mover, excluir
Files.copy(source, target); // fails if target exists
Files.copy(source, target, StandardCopyOption.REPLACE_EXISTING);
Files.copy(source, target,
StandardCopyOption.REPLACE_EXISTING,
StandardCopyOption.COPY_ATTRIBUTES); // copy mtime/owner too
Files.move(source, target, StandardCopyOption.REPLACE_EXISTING);
Files.move(source, target, StandardCopyOption.ATOMIC_MOVE); // rename within a filesystem; rename-or-fail
Files.delete(path); // throws if absent
boolean deleted = Files.deleteIfExists(path); // idempotentFiles.move com ATOMIC_MOVE é a ferramenta certa para "escrever em um arquivo temporário e depois substituir atomicamente o arquivo ativo." No mesmo sistema de arquivos mapeia para rename(2); o arquivo ativo passa de antigo para novo em um instante, sem estado intermediário. É assim que você constrói escritas seguras contra falhas:
Path tmp = path.resolveSibling(path.getFileName() + ".tmp");
Files.writeString(tmp, content, StandardCharsets.UTF_8);
Files.move(tmp, path, StandardCopyOption.ATOMIC_MOVE,
StandardCopyOption.REPLACE_EXISTING);Se a JVM morrer após writeString mas antes de move, o arquivo ativo permanece intocado.
Listar e percorrer
try (Stream<Path> entries = Files.list(directory)) {
entries.forEach(System.out::println); // direct children only
}
try (Stream<Path> tree = Files.walk(directory)) {
tree.filter(Files::isRegularFile).forEach(...); // recursive
}
try (Stream<Path> tree = Files.walk(directory, 2)) { // depth-limited
...
}
try (Stream<Path> found = Files.find(directory, Integer.MAX_VALUE,
(p, attrs) -> attrs.isRegularFile() && p.toString().endsWith(".log"))) {
...
}Sempre use try-with-resources em torno desses — o DirectoryStream subjacente fica aberto até o Stream fechar. Pule o fechamento e a JVM mantém um handle de diretório até a coleta de lixo notar, o que em um processo de longa duração é "nunca." O próximo capítulo, Java Walk File Tree, aprofunda o walker.
Por que este capítulo é curto
Files não precisa de muita narrativa. Cada método faz uma coisa, os nomes são descritivos, os parâmetros são Path, Charset e Option. A carga cognitiva está no catálogo — saber o que está disponível — não no comportamento de qualquer método individual. Percorra o Javadoc de java.nio.file.Files uma vez; volte quando precisar de um verbo que não lembre.
Um exemplo trabalhado: o ciclo de vida completo
O programa abaixo cria um diretório temporário, escreve um pequeno arquivo de texto com writeString, lê-o de volta com readString, acrescenta com a opção de abertura correta, copia o arquivo, move-o atomicamente, lista o diretório a cada etapa e, finalmente, faz a limpeza com deleteIfExists. É o ciclo de vida de arquivo Java do dia a dia comprimido em um único método main.
O que tirar da execução:
Files.writeString(...)abriu o arquivo, escreveu o conteúdo e o fechou — uma chamada ondejava.ioteria exigidoFileOutputStream+OutputStreamWriter(UTF-8)+BufferedWriter+try-with-resources. O padrão de truncar ao abrir é exatamente o que "salvar este conteúdo" quer. Quando você precisa manter o conteúdo existente, oStandardOpenOption.APPENDexplícito (passado junto comWRITE) é a substituição.Files.lines(log).filter(...)fez o mesmo trabalho de leitura em streaming queBufferedReader.lines()com a tubulação de abertura e fechamento embutida. Otry-with-resources em torno doStreamé o mecanismo de fechamento — pule-o e o handle do arquivo vaza. Todo método emFilesque retorna umStreamé fechável; trate-o dessa forma.- A etapa de cópia usou tanto
REPLACE_EXISTING(permitir sobrescrita) quantoCOPY_ATTRIBUTES(carregar o mtime/owner junto). SemCOPY_ATTRIBUTESo backup teria um mtime novo, o que importa para verificações de "este backup ainda está atual?".Files.copypadroniza para o comportamento conservador; você opta por qualquer outra coisa. - O bloco de movimentação atômica é o padrão de escrita segura: escreva o conteúdo em
target.tmp, depois useATOMIC_MOVEpara colocá-lo no nome ativo. Se a JVM travar no meio da escrita, o arquivo ativo fica inalterado; se o rename for bem-sucedido, o arquivo ativo muda em um instante. No mesmo sistema de arquivos isso mapeia pararename(2)— não há etapa de cópia. Use isso para qualquer arquivo onde os leitores nunca devem ver um estado meio escrito (configuração, arquivos de salvamento, assets gerados). Files.walk(dir)produziu umStream<Path>de cada entrada sob o diretório em ordem de busca em profundidade. A limpeza na etapa 10 ordenou ao contrário para que os filhos fossem excluídos antes dos pais — o mesmo truque que você usaria com uma exclusão recursiva real. (O helper completo de exclusão de árvore está no próximo capítulo sobwalkFileTree; a forma de streaming aqui é a versão mais curta para árvores pequenas.)
O que vem a seguir
Files cobriu as operações que atuam em um único arquivo ou em um único nível de diretório. O próximo capítulo, Java Walk File Tree, aprofunda a travessia de uma árvore de diretórios inteira — Files.walkFileTree, FileVisitor, pular subárvores, a API do padrão visitor que lida com os casos que a forma Stream não consegue.