W3docs

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çãoSignificado
READAbrir para leitura
WRITEAbrir para escrita
CREATECriar se ausente; não fazer nada se presente
CREATE_NEWCriar se ausente; falhar se presente
APPENDEscritas vão para o final do arquivo
TRUNCATE_EXISTINGLimpar conteúdo ao abrir
DELETE_ON_CLOSEExcluir quando o canal fechar (arquivos temporários)
SYNC / DSYNCBloquear 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 absent

Criaçã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);                       // idempotent

Files.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.

java— editable, runs on the server

O que tirar da execução:

  • Files.writeString(...) abriu o arquivo, escreveu o conteúdo e o fechou — uma chamada onde java.io teria exigido FileOutputStream + 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, o StandardOpenOption.APPEND explícito (passado junto com WRITE) é a substituição.
  • Files.lines(log).filter(...) fez o mesmo trabalho de leitura em streaming que BufferedReader.lines() com a tubulação de abertura e fechamento embutida. O try-with-resources em torno do Stream é o mecanismo de fechamento — pule-o e o handle do arquivo vaza. Todo método em Files que retorna um Stream é fechável; trate-o dessa forma.
  • A etapa de cópia usou tanto REPLACE_EXISTING (permitir sobrescrita) quanto COPY_ATTRIBUTES (carregar o mtime/owner junto). Sem COPY_ATTRIBUTES o backup teria um mtime novo, o que importa para verificações de "este backup ainda está atual?". Files.copy padroniza 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 use ATOMIC_MOVE para 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 para rename(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 um Stream<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 sob walkFileTree; 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.

Prática

Prática
Você quer sobrescrever o arquivo `/var/data/config.json` com um novo payload, mas os leitores nunca devem ver um estado meio escrito se a JVM travar no meio da escrita. Qual sequência de chamadas `Files` implementa o padrão de escrita segura?
Você quer sobrescrever o arquivo `/var/data/config.json` com um novo payload, mas os leitores nunca devem ver um estado meio escrito se a JVM travar no meio da escrita. Qual sequência de chamadas `Files` implementa o padrão de escrita segura?
Was this page helpful?