1. 简介
本文将介绍几种使用Java核心API和第三方库在指定目录(包括子目录)中查找特定扩展名文件的方法。我们将从传统的数组和列表方式,逐步过渡到流式处理等现代技术。
2. 构建文件过滤器
首先需要实现一个扩展名匹配的过滤器。这里我们使用Predicate
接口,并处理一些边界情况(如扩展名是否带点号):
public class MatchExtensionPredicate implements Predicate<Path> {
private final String extension;
public MatchExtensionPredicate(String extension) {
if (!extension.startsWith(".")) {
extension = "." + extension;
}
this.extension = extension.toLowerCase();
}
@Override
public boolean test(Path path) {
if (path == null) {
return false;
}
return path.getFileName()
.toString()
.toLowerCase()
.endsWith(extension);
}
}
关键点:
- 构造器自动补全点号并转为小写
test()
方法获取文件名后统一转为小写比较- ✅ 处理了null值和大小写问题
3. 使用Files.listFiles()遍历目录
这是Java早期提供的传统方法,通过递归实现目录遍历:
List<File> find(File startPath, String extension) {
List<File> matches = new ArrayList<>();
File[] files = startPath.listFiles();
if (files == null) {
return matches;
}
MatchExtensionPredicate filter = new MatchExtensionPredicate(extension);
for (File file : files) {
if (file.isDirectory()) {
matches.addAll(find(file, extension));
} else if (filter.test(file.toPath())) {
matches.add(file);
}
}
return matches;
}
⚠️ 注意:
- 需要手动处理递归逻辑
- 深层目录可能导致
StackOverflowError
- 大量文件可能触发
OutOfMemoryError
4. 使用Java 7+的Files.walkFileTree()
Java 7引入的NIO.2 API提供了更优雅的解决方案:
List<Path> find(Path startPath, String extension) throws IOException {
List<Path> matches = new ArrayList<>();
Files.walkFileTree(startPath, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attributes) {
if (new MatchExtensionPredicate(extension).test(file)) {
matches.add(file);
}
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFileFailed(Path file, IOException exc) {
return FileVisitResult.CONTINUE;
}
});
return matches;
}
优势:
- ✅ 自动处理递归遍历
- ✅ 通过
visitFileFailed
优雅处理访问异常 - ✅ 使用
Path
替代传统File
类
5. 使用Java 8+的Files.walk()
Java 8的Stream API提供了更简洁的写法:
Stream<Path> find(Path startPath, String extension) throws IOException {
return Files.walk(startPath)
.filter(new MatchExtensionPredicate(extension));
}
⚠️ 缺陷:
- 遇到第一个异常就会中断
- 目前无法优雅处理异常(JDK-8039910)
改进方案:结合FileVisitor
和Consumer
:
public class SimpleFileConsumerVisitor extends SimpleFileVisitor<Path> {
private final Predicate<Path> filter;
private final Consumer<Path> consumer;
public SimpleFileConsumerVisitor(MatchExtensionPredicate filter, Consumer<Path> consumer) {
this.filter = filter;
this.consumer = consumer;
}
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attributes) {
if (filter.test(file)) {
consumer.accept(file);
}
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException {
return FileVisitResult.CONTINUE;
}
}
使用方式:
void find(Path startPath, String extension, Consumer<Path> consumer) throws IOException {
MatchExtensionPredicate filter = new MatchExtensionPredicate(extension);
Files.walkFileTree(startPath, new SimpleFileConsumerVisitor(filter, consumer));
}
6. 使用Apache Commons IO的FileUtils.iterateFiles()
引入依赖:
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.15.1</version>
</dependency>
实现代码:
Iterator<File> find(Path startPath, String extension) {
if (!extension.startsWith(".")) {
extension = "." + extension;
}
return FileUtils.iterateFiles(
startPath.toFile(),
WildcardFileFilter.builder().setWildcards("*" + extension).get(),
TrueFileFilter.INSTANCE);
}
特点:
- ✅ 使用通配符过滤器简化匹配逻辑
- ✅
TrueFileFilter.INSTANCE
确保递归子目录 - ⚠️ 需要手动处理
Path
到File
的转换
7. 总结
本文对比了四种文件查找方案:
方法 | 优点 | 缺点 |
---|---|---|
listFiles() |
简单直接 | 需手动递归,性能风险 |
walkFileTree() |
异常处理完善 | 代码稍显冗长 |
Files.walk() |
最简洁流式API | 异常处理缺陷 |
Commons IO | 功能强大 | 需要额外依赖 |
推荐选择:
- 简单场景:
Files.walk()
- 生产环境:
walkFileTree()
或Commons IO - 避免使用:传统
listFiles()
递归方式
完整代码示例可在GitHub获取。