1. NIO2: conhecendo melhor
Nós já tivemos contato com NIO2, mas agora vamos revisar e aprofundar o conhecimento sobre essa biblioteca útil para trabalhar com arquivos e diretórios.
Antes, no Java, havia apenas a classe File. Ela conseguia verificar a existência do arquivo, criar e excluir arquivos e pastas, obter a lista de arquivos em um diretório. Mas tinha muitas limitações:
- trabalhar com caminhos era inconveniente, especialmente ao considerar diferentes sistemas operacionais (C:\Users\user\file.txt no Windows e /home/user/file.txt no Linux);
- não havia suporte adequado a links simbólicos, permissões e atributos de arquivos;
- as capacidades de percorrer a árvore de diretórios eram limitadas;
- o tratamento de erros deixava a desejar.
Com o surgimento do NIO2 (New Input/Output, versão 2) no Java 7, a vida do desenvolvedor ficou mais simples e agradável. Agora temos à disposição:
- a classe Path para trabalhar de forma conveniente com caminhos de arquivos e pastas;
- a classe Files, que oferece as operações principais: leitura, escrita, cópia, exclusão, obtenção de informações sobre arquivos;
- a interface FileVisitor e métodos como Files.walk, que permitem percorrer o sistema de arquivos de maneira fácil e flexível.
Por que isso é importante?
- Portabilidade: O mesmo código funciona no Windows, Linux e macOS, sem se preocupar com separadores (/ ou \).
- Segurança e praticidade: Mais informações sobre erros, menos mágica e menos surpresas inesperadas.
- Potência: É possível processar diretórios enormes e até percorrer recursivamente com filtragem e processamento paralelo.
2. Classes principais: Path e Files
Classe Path
Path — é uma representação moderna de um caminho para arquivo ou pasta. Ele não precisa apontar para um arquivo realmente existente — é apenas um caminho com o qual é conveniente trabalhar.
Obtendo um Path
import java.nio.file.Path;
import java.nio.file.Paths;
Path path1 = Paths.get("file.txt"); // caminho relativo
Path path2 = Paths.get("/home/user/file.txt"); // caminho absoluto
Path path3 = Path.of("mydir", "subdir", "file.txt"); // desde o Java 11+
Fato: Path é independente do sistema operacional. Esqueça colar strings manualmente com / ou \!
Conversão para string
System.out.println(path1.toString());
Obtendo o diretório pai e o nome do arquivo
Path parent = path1.getParent(); // pode ser null para caminhos relativos
Path fileName = path1.getFileName(); // apenas o nome do arquivo
Classe Files
Files — é um conjunto de métodos estáticos para todas as operações com arquivos e diretórios:
- Verificação de existência: Files.exists(path)
- Leitura e escrita de arquivos: Files.readAllBytes(path), Files.write(path, bytes)
- Obtenção de informações: Files.size(path), Files.getLastModifiedTime(path)
- Cópia, exclusão, movimentação: Files.copy, Files.delete, Files.move
Exemplos:
import java.nio.file.Files;
import java.nio.file.Path;
Path path = Path.of("file.txt");
if (Files.exists(path)) {
System.out.println("O arquivo existe!");
System.out.println("Tamanho: " + Files.size(path) + " bytes");
System.out.println("Última modificação: " + Files.getLastModifiedTime(path));
} else {
System.out.println("Arquivo não encontrado.");
}
3. Percorrendo o sistema de arquivos: Files.walk e companhia
O problema do modo antigo
No API antigo, para percorrer todos os arquivos em uma pasta e suas subpastas, era preciso escrever funções recursivas, verificar manualmente onde era arquivo e onde era pasta e garantir que não haveria recursão infinita. Isso não só era cansativo, como também muito suscetível a erros.
Abordagem moderna: Files.walk
Files.walk(Path start) retorna um Stream<Path> — um fluxo de todos os arquivos e pastas a partir do caminho indicado, incluindo todos os subdiretórios. Agora percorrer o sistema de arquivos é simplesmente trabalhar com streams!
Exemplo: listar todos os arquivos e pastas
import java.nio.file.*;
try (var paths = Files.walk(Path.of("mydir"))) {
paths.forEach(System.out::println);
}
Aqui serão impressos todos os caminhos: arquivos e pastas, a partir de mydir.
Exemplo: apenas arquivos (sem pastas)
try (var paths = Files.walk(Path.of("mydir"))) {
paths.filter(Files::isRegularFile)
.forEach(System.out::println);
}
O método Files.isRegularFile(path) retornará true apenas para arquivos normais (não pastas, não links simbólicos).
Exemplo: buscar arquivos por extensão
Suponha que precisamos encontrar todos os arquivos .txt no diretório e subdiretórios:
try (var paths = Files.walk(Path.of("mydir"))) {
paths.filter(Files::isRegularFile)
.filter(path -> path.toString().endsWith(".txt"))
.forEach(System.out::println);
}
Exemplo: calcular o tamanho total de todos os arquivos
long totalSize = 0;
try (var paths = Files.walk(Path.of("mydir"))) {
totalSize = paths.filter(Files::isRegularFile)
.mapToLong(path -> {
try {
return Files.size(path);
} catch (Exception e) {
System.err.println("Erro ao ler o tamanho: " + path);
return 0L;
}
})
.sum();
}
System.out.println("Tamanho total dos arquivos: " + totalSize + " bytes");
Importante!
- O método Files.walk retorna um stream que precisa ser fechado (ele implementa AutoCloseable). Por isso, use try-with-resources!
- Por padrão, a profundidade da varredura vai até o fim (todos os subdiretórios). É possível limitar a profundidade: Files.walk(path, maxDepth)
4. Tarefas práticas
Tarefa 1: encontrar todas as imagens no diretório
Precisamos encontrar todos os arquivos com extensão .jpg, .png, .gif na pasta images e imprimir seus nomes.
import java.nio.file.*;
import java.util.Set;
Set<String> extensions = Set.of(".jpg", ".png", ".gif");
try (var paths = Files.walk(Path.of("images"))) {
paths.filter(Files::isRegularFile)
.filter(path -> {
String name = path.getFileName().toString().toLowerCase();
return extensions.stream().anyMatch(name::endsWith);
})
.forEach(System.out::println);
}
Tarefa 2: copiar todos os arquivos .txt para outra pasta
import java.nio.file.*;
Path sourceDir = Path.of("src");
Path destDir = Path.of("dest");
try (var paths = Files.walk(sourceDir)) {
paths.filter(Files::isRegularFile)
.filter(path -> path.toString().endsWith(".txt"))
.forEach(path -> {
try {
Path relative = sourceDir.relativize(path);
Path target = destDir.resolve(relative);
Files.createDirectories(target.getParent());
Files.copy(path, target, StandardCopyOption.REPLACE_EXISTING);
System.out.println("Copiado: " + path + " -> " + target);
} catch (Exception e) {
System.err.println("Erro de cópia: " + path);
}
});
}
Aqui preservamos a estrutura dos subdiretórios.
5. Dicas úteis
Vantagens do NIO2
Portabilidade entre plataformas
Path lida sozinho com os separadores de pastas. Seu código funcionará da mesma forma no Windows, Linux e macOS.
Processamento com streams
Métodos como Files.walk retornam um fluxo (Stream<Path>) que pode ser filtrado, transformado, coletado em coleções — tudo o que o Stream API oferece.
Trabalho com diretórios grandes
O API antigo podia falhar se houvesse arquivos demais (por exemplo, 100 000 fotos). NIO2 lida bem com esses casos, pois não carrega tudo na memória de uma vez.
Suporte a links simbólicos, atributos e permissões de acesso
É possível saber se um caminho é um link simbólico (Files.isSymbolicLink(path)), obter permissões (Files.getPosixFilePermissions(path)), descobrir o proprietário do arquivo e muito mais.
Comparação entre o API antigo e o novo
| Operação | API antigo (File) | API novo (Path, Files) |
|---|---|---|
| Verificar existência | |
|
| Obter tamanho | |
|
| Listar arquivos na pasta | |
|
| Percurso recursivo | Recursão manual | |
| Copiar arquivo | file.renameTo() (problemático) | |
| Obter extensão | Fazer parsing da string | |
| Obter o pai | |
|
| Trabalhar com permissões | Quase nada | |
Características importantes
Verificação do tipo de arquivo
- Files.isRegularFile(path) — arquivo normal
- Files.isDirectory(path) — pasta
- Files.isSymbolicLink(path) — link simbólico
Trabalhar com diretórios grandes
- Evite juntar todos os caminhos em uma lista: trabalhe com streams (Stream<Path>) e processe conforme chegam.
- Ao terminar o processamento, feche o stream obrigatoriamente (try-with-resources).
Exceções
- Quase todos os métodos podem lançar IOException — não se esqueça de tratar os erros (ou propagá-los).
Limitar a profundidade da varredura
try (var paths = Files.walk(Path.of("mydir"), 2)) { // apenas 2 níveis
// ...
}
6. Erros comuns ao trabalhar com NIO2
Erro nº 1: esquecer de fechar o stream do walk. Se não usar try-with-resources, você pode ter vazamento de recursos — o fluxo do sistema de arquivos permanecerá aberto. Sempre use a construção try (var paths = Files.walk(...)) { ... }.
Erro nº 2: não verificar que o caminho é um diretório. Se você passar para Files.walk um caminho para arquivo em vez de pasta, pode obter um comportamento inesperado ou um erro.
Erro nº 3: não tratar as exceções. Praticamente todos os métodos do NIO2 podem lançar IOException. Não ignore esses erros — ao menos mostre uma mensagem ao usuário ou registre no log.
Erro nº 4: confusão com separadores de caminho. Se você estiver colando caminhos manualmente com / ou \, não faça isso! Use Path.of(...) ou resolve(...) — eles cuidarão disso para você.
Erro nº 5: tentar carregar um diretório enorme “na memória”. Não junte todos os caminhos em uma lista quando houver muitos arquivos — trabalhe com streams (Stream<Path>) e processe-os conforme chegam.
Erro nº 6: esquecer da portabilidade entre plataformas. Não faça hardcode de caminhos absolutos com estilo de Windows ou Unix. Use Path e operações como resolve/relativize — elas farão o correto em qualquer sistema operacional.
GO TO FULL VERSION