W3docs

Percorrendo Árvores de Ficheiros em Java

Percorra diretórios recursivamente em Java com Files.walk, Files.find e a interface FileVisitor.

O capítulo anterior terminou com Files.walk(dir) — a forma Stream<Path> de "dê-me todos os ficheiros sob este diretório." Essa é a ferramenta rápida para o caso comum. Este capítulo aborda a alternativa de nível mais baixo, Files.walkFileTree, que permite controlar a travessia de formas que a forma stream não consegue: tratar erros de I/O por ficheiro, ignorar subárvores inteiras durante a caminhada, executar código na saída de um diretório além da entrada, e curto-circuitar numa correspondência.

Use Files.walk para "listar tudo." Use Files.walkFileTree para "fazer algo em cada passo, com controlo sobre o passo."

Três APIs de caminhada

O catálogo, por ordem de frequência de uso:

APIRetornaQuando
Files.walk(dir)Stream<Path>Mais comum — filter/map/foreach sobre cada entrada
Files.find(dir, depth, biPredicate)Stream<Path>Igual, com um predicado ciente de atributos (isDirectory, mtime)
Files.walkFileTree(dir, visitor)Path (o início)Necessita de hooks pré/pós-visita, tratamento de erros por ficheiro, ou para abortar a caminhada

Os dois primeiros são suficientes para 90% do código "encontre todos os ficheiros .log". walkFileTree é o que se usa quando a resposta é "e depois apagar o diretório" ou "parar de caminhar assim que encontrar o que procuro."

FileVisitor e SimpleFileVisitor

Files.walkFileTree recebe um FileVisitor<Path> — uma interface com quatro métodos que o walker chama em momentos específicos:

FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs);    // entering a directory
FileVisitResult visitFile(Path file, BasicFileAttributes attrs);            // each non-directory entry
FileVisitResult visitFileFailed(Path file, IOException exc);                // I/O failure on a specific file
FileVisitResult postVisitDirectory(Path dir, IOException exc);              // leaving the directory (after all children)

A ordem importa: para um diretório d com filhos [a, b/, c], as chamadas são preVisitDirectory(d), visitFile(a), preVisitDirectory(b), ... postVisitDirectory(b), visitFile(c), postVisitDirectory(d). O hook post* é o que torna possível a eliminação recursiva — não é possível apagar um diretório antes de apagar o seu conteúdo.

SimpleFileVisitor<Path> é a classe auxiliar que implementa os quatro métodos com valores padrão sensatos (continuar em caso de sucesso, lançar excepção em caso de falha). Faça uma subclasse e substitua apenas os métodos de que necessita:

class LogPrinter extends SimpleFileVisitor<Path> {
  @Override public FileVisitResult visitFile(Path f, BasicFileAttributes a) {
    System.out.println(f);
    return FileVisitResult.CONTINUE;
  }
}
Files.walkFileTree(root, new LogPrinter());

Este é o visitor mínimo viável.

FileVisitResult: quatro sinais

Cada método do visitor retorna um FileVisitResult a informar o walker sobre o que fazer a seguir:

ValorEfeito
CONTINUENormal — ir para a próxima entrada
SKIP_SUBTREE(de preVisitDirectory apenas) Ignorar este diretório e os seus filhos completamente
SKIP_SIBLINGSParar de visitar o restante do diretório atual; retomar no próximo irmão do pai
TERMINATEParar a caminhada completamente

SKIP_SUBTREE é o que se usa com mais frequência: "não descer para .git/ ou node_modules/." Retorne-o de preVisitDirectory quando o nome do diretório corresponder e o walker ignorará tanto o diretório como os seus filhos:

@Override public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes a) {
  String name = dir.getFileName() == null ? "" : dir.getFileName().toString();
  if (name.equals(".git") || name.equals("node_modules")) {
    return FileVisitResult.SKIP_SUBTREE;
  }
  return FileVisitResult.CONTINUE;
}

TERMINATE é o sinal "encontrado, parar" — útil quando se procura o primeiro ficheiro correspondente e não se quer caminhar o restante:

@Override public FileVisitResult visitFile(Path f, BasicFileAttributes a) {
  if (f.getFileName().toString().equals("target.txt")) {
    found = f;
    return FileVisitResult.TERMINATE;
  }
  return FileVisitResult.CONTINUE;
}

A forma Stream não consegue fazer isto — Files.walk(...).filter(...).findFirst() curto-circuita, mas apenas depois de o walker já ter enumerado cada entrada do diretório no stream. Para uma árvore profunda onde a correspondência é superficial, walkFileTree é significativamente mais rápido.

Tratamento de erros por ficheiro

visitFile e preVisitDirectory só são chamados quando o JDK conseguiu ler a entrada. Se um único ficheiro for ilegível (permissão negada, symlink pendente, condição de corrida onde foi apagado durante a caminhada), visitFileFailed é chamado em vez disso com a excepção. Por padrão SimpleFileVisitor relança — isso aborta a caminhada:

@Override public FileVisitResult visitFileFailed(Path f, IOException e) throws IOException {
  throw e;                                          // default behaviour
}

Para um walker tolerante (registar e continuar), substitua-o:

@Override public FileVisitResult visitFileFailed(Path f, IOException e) {
  System.err.println("skipping " + f + ": " + e.getMessage());
  return FileVisitResult.CONTINUE;
}

Files.walk(...) não tem este hook — lança uma UncheckedIOException de dentro do stream no momento em que encontra uma entrada inválida, e o stream fica inutilizável após isso. Para scanners de longa duração em sistemas de ficheiros que não controla totalmente, é mais uma razão para usar walkFileTree.

O caso de uso canónico: eliminação recursiva

Files.delete só funciona em diretórios vazios. Para remover uma árvore é necessário apagar primeiro as folhas, depois os diretórios que as continham. walkFileTree tem a forma certa para isto — visitFile apaga o ficheiro, postVisitDirectory apaga o diretório depois de todos os seus filhos terem sido removidos:

Files.walkFileTree(root, new SimpleFileVisitor<Path>() {
  @Override public FileVisitResult visitFile(Path f, BasicFileAttributes a) throws IOException {
    Files.delete(f);
    return FileVisitResult.CONTINUE;
  }
  @Override public FileVisitResult postVisitDirectory(Path d, IOException e) throws IOException {
    if (e != null) throw e;                          // propagate I/O failures from descent
    Files.delete(d);
    return FileVisitResult.CONTINUE;
  }
});

Esta é a receita do JDK para "apagar uma árvore de diretórios." Toda a base de código que precisa disto acaba com alguma versão deste bloco de 10 linhas. Guarde uma cópia numa classe utilitária e reutilize.

Por padrão, Files.walkFileTree e Files.walk não seguem links simbólicos. Esse é o padrão seguro: evita loops infinitos num symlink que aponta para o seu próprio ancestral. Para segui-los, passe FileVisitOption.FOLLOW_LINKS:

Files.walkFileTree(root, EnumSet.of(FileVisitOption.FOLLOW_LINKS),
    Integer.MAX_VALUE, visitor);

Ao optar por isso, o walker deteta ciclos automaticamente — rastreia as chaves de diretório visitadas e para se a mesma aparecer novamente com FileSystemLoopException. Essa é a única forma de percorrer uma árvore com links sem escrever a deteção de ciclos manualmente.

Um exemplo prático: impressão de árvore, skip-subtree, eliminação recursiva

O programa abaixo constrói uma pequena árvore de diretórios com alguns subdiretórios (um dos quais queremos ignorar), ficheiros a vários níveis, e depois percorre-a de três formas. Primeiro, um impressor de árvore com SimpleFileVisitor que ignora .git. Segundo, um "encontrar a primeira correspondência" com TERMINATE. Terceiro, o padrão canónico de eliminação recursiva que remove toda a árvore no final.

java— editable, runs on the server

O que retirar da execução:

  • O hook preVisitDirectory retornou SKIP_SUBTREE no momento em que viu .git. O walker nunca desceu para o diretório; o ficheiro config sob ele nunca foi visitado. Essa é a ferramenta certa para "ignorar estes diretórios convencionais" — .git, node_modules, target, dist, ou qualquer outro que o projeto não queira percorrer. A forma Stream<Path> não consegue fazer isto sem produzir as entradas e filtrá-las, o que ainda implica o custo da leitura do diretório.
  • A ordem das chamadas para sub/ foi preVisitDirectory(sub)visitFile(b.txt)preVisitDirectory(nested)visitFile(c.txt)postVisitDirectory(nested)postVisitDirectory(sub). Os hooks post* disparam depois de todos os descendentes terem sido processados — esse é o contrato depth-first, e é o que torna possível o padrão de eliminação recursiva.
  • A caminhada "encontrar o primeiro" retornou TERMINATE de visitFile no momento em que c.txt apareceu. Tudo depois disso — as entradas restantes em nested/, o restante de sub/, o restante de root/ — nunca foi visitado. Numa árvore pequena a economia é invisível; numa árvore profunda onde a correspondência é superficial, é a diferença entre O(n) e O(profundidade-da-correspondência).
  • A eliminação recursiva teve duas partes. visitFile apagou as folhas; postVisitDirectory apagou os diretórios (agora vazios). A ordem depth-first do walker garantiu que cada filho foi visitado antes do postVisitDirectory do seu pai, portanto Files.delete(d) sempre viu um diretório vazio. Tentar apagar o diretório em preVisitDirectory falharia porque os filhos ainda estão lá; tentar apagá-lo com Files.delete(root) no final falharia pela mesma razão. O hook post* é o ponto central da API visitor.
  • Ao longo de tudo, SimpleFileVisitor foi a classe base e substituímos apenas os métodos de que precisávamos. visitFileFailed ficou no seu valor padrão (lançar excepção), o que para estas demonstrações com ficheiros temporários é adequado. Para um scanner num sistema de ficheiros real que não controla totalmente — digamos, um scanner antivírus a percorrer /, onde os ficheiros podem ser apagados durante a caminhada — substitua visitFileFailed para registar e CONTINUE.

O que vem a seguir

A Parte 13 termina aqui. Ficheiros foram escritos, lidos, abertos, copiados, movidos, apagados, percorridos, serializados. Streams foram tamponados, decorados, formatados, mapeados, canalizados. A próxima parte, Date and Time, passa para um problema completamente diferente: representar instantes, durações, datas de calendário, fusos horários, e a formatação e análise destes — java.time, a API moderna que substituiu java.util.Date e Calendar.

Prática

Prática
Precisa de apagar uma árvore de diretórios contendo 50 ficheiros em 10 subdiretórios aninhados. Qual implementação do hook `FileVisitor` remove cada diretório apenas depois de os seus filhos terem sido removidos?
Precisa de apagar uma árvore de diretórios contendo 50 ficheiros em 10 subdiretórios aninhados. Qual implementação do hook `FileVisitor` remove cada diretório apenas depois de os seus filhos terem sido removidos?
Was this page helpful?