1. Introdução
Vamos relembrar um exemplo de varredura com Files.walk():
Path start = Paths.get("my-folder");
try (Stream<Path> stream = Files.walk(start)) {
stream.forEach(System.out::println);
}
Isso funciona muito bem quando é necessário apenas percorrer arquivos e diretórios e fazer algo com eles. Mas e se a tarefa for mais complexa?
- É preciso excluir recursivamente um diretório junto com todos os arquivos e subdiretórios (e não apenas se ele estiver vazio).
- É preciso copiar ou mover um diretório recursivamente.
- É preciso coletar estatísticas (por exemplo, somar o tamanho total de todos os arquivos, agrupar arquivos por extensão).
- É preciso tratar erros (por exemplo, se não houver acesso a algum diretório, não queremos que tudo quebre).
Nesses casos, a Stream API já não é tão conveniente: você acaba colocando try-catch dentro de lambdas, precisa cuidar da ordem da varredura (por exemplo, primeiro excluir arquivos e depois diretórios), e o código fica pouco legível.
Para essas tarefas foi criado o mecanismo de varredura da árvore do sistema de arquivos com FileVisitor.
2. Interface FileVisitor: como ela funciona
A interface FileVisitor<T> é um “manipulador de eventos” que recebe notificações sobre cada arquivo e diretório visitado durante a varredura do sistema de arquivos.
Quando você chama Files.walkFileTree(start, visitor), o Java inicia a varredura da árvore a partir do caminho indicado e, a cada etapa, chama o método correspondente do seu FileVisitor.
Métodos principais da interface FileVisitor
Veja como é a interface:
public interface FileVisitor<T> {
FileVisitResult preVisitDirectory(T dir, BasicFileAttributes attrs) throws IOException;
FileVisitResult visitFile(T file, BasicFileAttributes attrs) throws IOException;
FileVisitResult visitFileFailed(T file, IOException exc) throws IOException;
FileVisitResult postVisitDirectory(T dir, IOException exc) throws IOException;
}
- preVisitDirectory — chamado antes de entrar em um diretório.
- visitFile — chamado para cada arquivo.
- visitFileFailed — chamado se não foi possível acessar um arquivo.
- postVisitDirectory — chamado após sair de um diretório (ou seja, depois de processar todos os seus arquivos e subdiretórios).
Cada um desses métodos retorna um valor do tipo FileVisitResult, que determina como continuar a varredura:
- FileVisitResult.CONTINUE — continuar a varredura.
- FileVisitResult.SKIP_SUBTREE — pular o diretório atual e tudo dentro dele.
- FileVisitResult.SKIP_SIBLINGS — pular os “irmãos” restantes (arquivos e diretórios neste nível).
- FileVisitResult.TERMINATE — encerrar completamente a varredura.
Classe SimpleFileVisitor
Implementar todos os quatro métodos toda vez é cansativo, especialmente se você precisa de apenas um ou dois. Por isso, o Java oferece a classe adaptadora conveniente SimpleFileVisitor: ela já implementa todos os métodos com comportamento “padrão” (apenas CONTINUE), e você pode sobrescrever apenas os necessários.
Exemplo:
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
public class MyVisitor extends SimpleFileVisitor<Path> {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
System.out.println("Arquivo: " + file);
return FileVisitResult.CONTINUE;
}
}
3. Usando Files.walkFileTree: exemplo básico
Exemplo 1: listando todos os arquivos e diretórios
import java.io.IOException;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
public class TreePrinter {
public static void main(String[] args) throws IOException {
Path start = Paths.get("my-folder");
Files.walkFileTree(start, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) {
System.out.println("Diretório: " + dir);
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
System.out.println(" Arquivo: " + file);
return FileVisitResult.CONTINUE;
}
});
}
}
Saída:
Diretório: my-folder
Arquivo: my-folder/file1.txt
Arquivo: my-folder/file2.txt
Diretório: my-folder/subdir
Arquivo: my-folder/subdir/nested.txt
Como você pode ver, a varredura é em “profundidade”: primeiro entramos no diretório, depois processamos seus arquivos e então vamos para os subdiretórios.
4. Exemplo: exclusão recursiva de diretório
Uma das tarefas mais comuns: excluir um diretório junto com todo o seu conteúdo. Se você tentar excluir um diretório com Files.delete(path) e ele não estiver vazio, receberá uma exceção. É preciso primeiro excluir todos os arquivos e subdiretórios e, só depois, o próprio diretório.
Veja como fazer isso usando FileVisitor:
import java.io.IOException;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
public class RecursiveDelete {
public static void main(String[] args) throws IOException {
Path dirToDelete = Paths.get("test-folder");
Files.walkFileTree(dirToDelete, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
Files.delete(file); // exclui o arquivo
System.out.println("Arquivo excluído: " + file);
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
Files.delete(dir); // exclui o diretório após remover tudo dentro dele
System.out.println("Diretório excluído: " + dir);
return FileVisitResult.CONTINUE;
}
});
}
}
Ponto importante:
A exclusão de diretórios ocorre em postVisitDirectory, ou seja, depois de excluir todo o conteúdo. Se tentássemos excluir o diretório antes de excluir os arquivos dentro dele, teríamos um erro.
5. Exemplo: cálculo do tamanho total de todos os arquivos em um diretório
Agora vamos escrever um FileVisitor que soma o tamanho total de todos os arquivos em um diretório e seus subdiretórios.
import java.io.IOException;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
public class DirectorySizeCalculator {
private static long totalSize = 0;
public static void main(String[] args) throws IOException {
Path start = Paths.get("my-folder");
Files.walkFileTree(start, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
totalSize += attrs.size();
return FileVisitResult.CONTINUE;
}
});
System.out.println("Tamanho total: " + totalSize + " bytes");
}
}
Observe:
Usamos o campo totalSize para acumular o tamanho. Em aplicativos reais, é melhor evitar campos estáticos e passar variáveis por objetos, mas para simplificar o exemplo, fica assim.
6. Exemplo: busca de arquivos por padrão (extensão) com FileVisitor
Suponha que você precise encontrar todos os arquivos .txt no diretório e subdiretórios e imprimir a lista.
import java.io.IOException;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
public class TxtFileFinder {
public static void main(String[] args) throws IOException {
Path start = Paths.get("my-folder");
Files.walkFileTree(start, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
if (file.getFileName().toString().endsWith(".txt")) {
System.out.println("Arquivo .txt encontrado: " + file);
}
return FileVisitResult.CONTINUE;
}
});
}
}
Se você quiser coletar a lista de arquivos encontrados, pode criar uma lista:
import java.util.ArrayList;
import java.util.List;
// dentro do main:
List<Path> txtFiles = new ArrayList<>();
Files.walkFileTree(start, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
if (file.getFileName().toString().endsWith(".txt")) {
txtFiles.add(file);
}
return FileVisitResult.CONTINUE;
}
});
System.out.println("Total encontrado: " + txtFiles.size());
7. Tratamento de erros e particularidades da varredura
O que fazer se não houver acesso a um arquivo ou diretório?
Às vezes, durante a varredura, encontramos arquivos ou diretórios aos quais não há acesso (por exemplo, falta de permissões ou o arquivo está em uso por outro processo). Nesse caso, o método visitFileFailed será chamado.
Exemplo:
@Override
public FileVisitResult visitFileFailed(Path file, IOException exc) {
System.err.println("Erro de acesso ao arquivo: " + file + " (" + exc + ")");
return FileVisitResult.CONTINUE; // continuar a varredura apesar do erro
}
Se quiser interromper a varredura em caso de erro, retorne TERMINATE.
Como pular um diretório inteiro?
Se você não quer entrar em um diretório específico (por exemplo, .git ou node_modules), você pode retornar SKIP_SUBTREE no método preVisitDirectory:
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) {
if (dir.getFileName().toString().equals("node_modules")) {
return FileVisitResult.SKIP_SUBTREE; // não entrar nesse diretório nem em seus subdiretórios
}
return FileVisitResult.CONTINUE;
}
8. Prática: vamos implementar nosso próprio FileVisitor
Vamos implementar um FileVisitor que:
- Encontra todos os arquivos com extensão .java no diretório e subdiretórios,
- Conta quantos são,
- Soma o tamanho total desses arquivos.
import java.io.IOException;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
public class JavaFilesStats {
public static void main(String[] args) throws IOException {
Path start = Paths.get("src"); // por exemplo, o código-fonte do projeto
class JavaFileVisitor extends SimpleFileVisitor<Path> {
int count = 0;
long totalSize = 0;
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
if (file.getFileName().toString().endsWith(".java")) {
count++;
totalSize += attrs.size();
System.out.println("Arquivo .java encontrado: " + file);
}
return FileVisitResult.CONTINUE;
}
}
JavaFileVisitor visitor = new JavaFileVisitor();
Files.walkFileTree(start, visitor);
System.out.println("Total de arquivos .java: " + visitor.count);
System.out.println("Tamanho total: " + visitor.totalSize + " bytes");
}
}
9. Erros comuns ao usar FileVisitor
Erro nº 1: tentar excluir um diretório antes de excluir os arquivos dentro dele. Se você chamar Files.delete(dir) em preVisitDirectory, terá uma exceção — primeiro é preciso excluir todos os arquivos e subdiretórios e só então o próprio diretório (faça isso em postVisitDirectory).
Erro nº 2: esquecer de tratar erros de acesso. Se você não sobrescrever visitFileFailed, o programa pode terminar inesperadamente ao encontrar um arquivo protegido. É melhor registrar o erro explicitamente e continuar a varredura.
Erro nº 3: esperar que a operação “ocultar arquivo” funcione igual em todos os sistemas operacionais. No Linux e no macOS, um arquivo é considerado oculto se começar com ponto (.gitignore); no Windows, depende de um atributo especial. Não confunda essas abordagens.
Erro nº 4: usar campos estáticos para acumular resultados em aplicações multithread. Se você executar várias varreduras em paralelo, campos estáticos causarão confusão. Prefira campos de instância (ou de uma classe local, como no exemplo acima).
Erro nº 5: esquecer de fechar recursos ao trabalhar com streams dentro do FileVisitor. Se o seu FileVisitor ler/gravar arquivos, use try-with-resources para evitar vazamentos.
GO TO FULL VERSION