1. 遍历目录中的文件
获取文件列表:Files.list 与 Files.walk
在 Java 中有两个非常好用的方法可用于处理文件夹内容:
- Files.list(Path) — 返回给定目录顶层(不递归)的所有文件和文件夹的流(Stream<Path>)。
- Files.walk(Path) — 返回目录及其所有子目录(递归)中的所有文件和文件夹的流。
示例:输出目录 data 中的所有文件和文件夹
import java.nio.file.*;
import java.io.IOException;
public class ListFilesExample {
public static void main(String[] args) throws IOException {
Path dir = Paths.get("data");
// 获取所有顶层文件和文件夹的流
try (var stream = Files.list(dir)) {
stream.forEach(System.out::println);
}
}
}
如果你想递归遍历所有子文件夹,请使用 Files.walk:
try (var stream = Files.walk(dir)) {
stream.forEach(System.out::println);
}
注意:
Files.walk 在结构很大的情况下可能返回非常多的文件——不要在磁盘根目录上运行,否则你会看到“整个宇宙的清单”!
可视化:list 与 walk 的区别
| 方法 | 返回内容 |
|---|---|
|
仅当前文件夹的内容 |
|
所有文件和文件夹,包括嵌套的 |
2. 文件过滤
使用流(Stream<Path>)的最大优势之一是可以按任意条件过滤文件:扩展名、名称、大小、创建日期等。
按扩展名过滤
假设我们只需要 .txt 文件,可以这样过滤:
try (var stream = Files.list(dir)) {
stream
.filter(path -> path.toString().endsWith(".txt"))
.forEach(System.out::println);
}
按名称过滤
获取文件名以 "report" 开头的所有文件:
stream
.filter(path -> path.getFileName().toString().startsWith("report"))
.forEach(System.out::println);
按大小过滤
只获取“较大”的文件(大于 1 MB):
stream
.filter(path -> {
try {
return Files.size(path) > 1_000_000;
} catch (IOException e) {
return false;
}
})
.forEach(System.out::println);
按日期过滤
例如,最近 7 天内修改的文件:
import java.time.*;
import java.nio.file.attribute.*;
stream
.filter(path -> {
try {
FileTime lastModified = Files.getLastModifiedTime(path);
return lastModified.toInstant().isAfter(Instant.now().minus(Duration.ofDays(7)));
} catch (IOException e) {
return false;
}
})
.forEach(System.out::println);
组合过滤:多条件一起
可以像“点菜”一样组合过滤条件:例如,筛选一个月之前且大于 10 KB 的所有 .log 文件:
stream
.filter(path -> path.toString().endsWith(".log"))
.filter(path -> {
try {
return Files.size(path) > 10_000;
} catch (IOException e) {
return false;
}
})
.filter(path -> {
try {
FileTime lastModified = Files.getLastModifiedTime(path);
return lastModified.toInstant().isBefore(Instant.now().minus(Duration.ofDays(30)));
} catch (IOException e) {
return false;
}
})
.forEach(System.out::println);
3. 批量复制与删除文件
批量操作很简单:先获取文件流,然后对每个元素应用所需函数。关键是不要忽略可能的错误(例如,文件被占用或目标已存在)。
将一个文件夹中的所有文件复制到另一个文件夹
import java.nio.file.StandardCopyOption;
Path sourceDir = Paths.get("data");
Path targetDir = Paths.get("backup");
try (var stream = Files.list(sourceDir)) {
stream.forEach(path -> {
Path targetPath = targetDir.resolve(path.getFileName());
try {
Files.copy(path, targetPath, StandardCopyOption.REPLACE_EXISTING);
System.out.println("已复制:" + path.getFileName());
} catch (IOException e) {
System.out.println("复制出错 " + path.getFileName() + ":" + e.getMessage());
}
});
}
删除所有临时文件(*.tmp)
try (var stream = Files.list(dir)) {
stream
.filter(path -> path.toString().endsWith(".tmp"))
.forEach(path -> {
try {
Files.delete(path);
System.out.println("已删除:" + path.getFileName());
} catch (IOException e) {
System.out.println("删除出错 " + path.getFileName() + ":" + e.getMessage());
}
});
}
使用 Stream API 处理路径集合
try (var stream = Files.list(dir)) {
stream
.filter(path -> path.toString().endsWith(".txt"))
.sorted((p1, p2) -> {
try {
return Long.compare(Files.size(p2), Files.size(p1)); // 按大小,降序
} catch (IOException e) {
return 0;
}
})
.limit(5) // 仅取最大的 5 个
.forEach(System.out::println);
}
4. 实践任务
查找并删除早于指定日期的文件
删除一年内未修改的所有文件:
try (var stream = Files.list(dir)) {
stream
.filter(path -> {
try {
FileTime lastModified = Files.getLastModifiedTime(path);
return lastModified.toInstant().isBefore(Instant.now().minus(Duration.ofDays(365)));
} catch (IOException e) {
return false;
}
})
.forEach(path -> {
try {
Files.delete(path);
System.out.println("已删除:" + path.getFileName());
} catch (IOException e) {
System.out.println("删除出错 " + path.getFileName() + ":" + e.getMessage());
}
});
}
按模板批量重命名文件
假设需要为每个 .txt 文件添加前缀 "old_":
try (var stream = Files.list(dir)) {
stream
.filter(path -> path.toString().endsWith(".txt"))
.forEach(path -> {
Path newPath = path.resolveSibling("old_" + path.getFileName());
try {
Files.move(path, newPath);
System.out.println("已重命名:" + path.getFileName() + " -> " + newPath.getFileName());
} catch (IOException e) {
System.out.println("重命名出错 " + path.getFileName() + ":" + e.getMessage());
}
});
}
复制所有子文件夹中的所有文件(递归)
使用 Files.walk 遍历所有子文件夹中的文件:
try (var stream = Files.walk(sourceDir)) {
stream
.filter(Files::isRegularFile)
.forEach(path -> {
Path relative = sourceDir.relativize(path);
Path targetPath = targetDir.resolve(relative);
try {
Files.createDirectories(targetPath.getParent());
Files.copy(path, targetPath, StandardCopyOption.REPLACE_EXISTING);
System.out.println("已复制:" + path + " -> " + targetPath);
} catch (IOException e) {
System.out.println("复制出错 " + path + ":" + e.getMessage());
}
});
}
5. 重要注意事项与细节
批量操作中的错误处理
在批量操作中几乎总会遇到“问题”文件:无权限、文件被占用、路径过长、目标已存在等。如果不在循环内部处理异常,程序可能会在第一个失败处“崩溃”。因此请务必在 forEach 内使用 try-catch,对单个文件的错误进行“吞掉/记录”,但继续处理其他文件。
递归对性能与调用栈的影响
对大型目录使用 Files.walk 时,如果手动编写递归,遍历路径可能会非常长。好在该方法本身实现高效,不会导致栈溢出。但如果你在编写自己的递归函数来删除/复制目录,请小心:对于巨大且深层嵌套的结构,调用栈可能会“爆掉”。
处理大型文件夹
- 谨慎使用 Files.walk:如果文件夹中有成千上万个文件,这会耗时且占用大量内存。
- 如果只需处理顶层内容——请使用 Files.list。
- 按名称匹配查找文件可使用(endsWith、matches 等)进行过滤。
示例:“清理文件夹中的临时文件”
Path tempDir = Paths.get("data/tmp");
try (var stream = Files.walk(tempDir)) {
stream
.filter(Files::isRegularFile)
.filter(path -> path.toString().endsWith(".tmp"))
.forEach(path -> {
try {
Files.delete(path);
System.out.println("已删除:" + path);
} catch (IOException e) {
System.out.println("删除出错 " + path + ":" + e.getMessage());
}
});
}
批量操作的主要方法
| 操作 | 方法/思路 | 要点 |
|---|---|---|
| 获取所有文件 | |
walk—递归,list—仅顶层 |
| 按扩展名过滤 | |
可与其他过滤条件组合 |
| 复制一组文件 | |
请确认目标文件夹已存在 |
| 删除一组文件 | |
最好为每次操作使用 try-catch |
| 批量重命名 | |
重命名本质上是带新名称的 move |
6. 批量操作中的常见错误
错误 1:在 forEach 内未处理的异常。
这很常见:如果不把每个操作包在 try-catch 中,程序可能会在第一个问题文件上“崩溃”,其余的将不会被处理。
错误 2:把文件夹当作文件来删除/复制(或反之)。
方法 Files.delete 和 Files.copy 对文件与文件夹的处理不同。不要混淆!例如,尝试用标准方法删除非空文件夹会导致错误。
错误 3:复制时错误地构造目标路径。
如果构造目标路径时不考虑目录结构(例如未使用 relativize),可能会覆盖文件,或得到错误的归档结构。
错误 4:同时打开过多的文件。
如果在循环中打开读/写流,请不要忘记关闭!最好使用 try-with-resources。
错误 5:递归遍历未限制深度。
对非常大且很深的文件夹使用 Files.walk 可能引发性能和内存问题。如果需要限制深度——请使用带深度参数的重载:Files.walk(dir, depth)。
错误 6:无意中忽略隐藏/系统文件。
如果应用处理用户文件,可能会不小心涉及隐藏或系统文件。要过滤它们,请使用方法 Files.isHidden(path)。
GO TO FULL VERSION