做Java开发这几年,我其实很少碰到完全不用碰文件系统的项目。小到读取配置文件、生成日志文件,大到构建工具扫描源码、数据同步任务比对目录差异,本质都绕不开同一个问题:怎么把一个目录及其所有子目录里的文件,完整、可靠、不卡死、不崩堆地遍历出来。
这个需求听起来简单,但真写起来,坑比想象中多。我第一次写目录遍历,是用File.listFiles()加递归,当时觉得“这有什么难的”。后来在线上环境扫一个几十万文件的目录,直接内存抖动、句柄泄漏,才意识到文件遍历其实是个非常考验基本功的话题。而且这个问题在面试里几乎必考,尤其是“Java怎么遍历目录和子目录”这种看似基础、实际能问出很多深度的八股题。
这篇内容我就把目录遍历这件事从头到尾理一遍:从最基础的File递归,到NIO的Files.walk和Files.walkFileTree,再到性能对比、异常处理、面试问答,全部用我做过的项目和踩过的坑来讲清楚。
1. 理清头绪:目录遍历在业务里的真实场景和方案选型
1.1 什么时候需要遍历目录与子目录
先别急着看代码,想一下你到底要解决什么问题。我接触过的目录遍历需求,基本可以归成几类:
- 收集类需求:把某个目录下所有文件名列出来,比如生成文件清单、导出目录结构、索引全量文件。这类需求最常见,对性能要求中等,但要求结果完整。
- 统计类需求:统计目录下所有文件的总大小、文件数量,或者按类型分组统计。这类需求对准确性要求高,遍历过程中一个文件都不能漏,漏一个大小就不对。
- 操作类需求:批量改名、批量压缩、批量删除过期文件。删除非空目录尤其典型,必须先删文件再删目录,层级越深越容易出错。
- 搜索类需求:按文件名后缀、修改时间、内容关键字查找特定文件。这类需求通常要配合过滤条件,而且往往要跳过部分目录,比如跳过
.git、node_modules、target这类构建产物目录。
你要先确认自己属于哪一类,因为不同场景适合的遍历实现是完全不一样的。只列文件名和要统计文件大小,代码路径差得非常多;要在遍历中“跳过某些目录”和“容忍某个文件读不出来”,那就直接决定了你是用简单递归还是用FileVisitor。很多人的代码跑着跑着突然崩了,就是因为一开始没看清楚自己的场景。
1.2 Java给我们的四类遍历手段怎么选
Java生态里做目录遍历,主流的手段其实就四类,我直接列个表把它们的核心差异摆出来。
| 遍历方式 | 核心API | 是否支持剪枝 | 异常处理 | 内存模型 | 适用场景 |
|---|---|---|---|---|---|
| File递归 | File.listFiles() + 递归方法 |
自己写逻辑控制 | 容易因空指针/异常中断 | 每层递归创建数组,层级越深栈压力越大 | 小目录、简单需求、JDK 7之前的老项目 |
| 目录流 | Files.newDirectoryStream() |
不支持直接剪枝 | 需要自己处理IOException |
按目录迭代,比listFiles省内存 |
只遍历单层目录,配合递归做完整遍历 |
| 文件树流 | Files.walk() |
用filter延迟过滤,但无法中断子树 |
遇到访问异常会抛UncheckedIOException |
Stream延迟加载,但会持有目录流句柄 | 简单过滤、收集文件名,代码最简洁 |
| 回调式访问 | Files.walkFileTree() + FileVisitor |
支持SKIP_SUBTREE精准剪枝 |
每个访问点都可控,可返回CONTINUE跳过 |
按事件回调,无中间集合 | 复杂场景、生产级批量任务、需要容错 |
从表格能看出来,File递归是兜底方案,Files.walk是便捷方案,Files.walkFileTree是完整方案。现在新写的代码,我几乎不再用File递归做核心逻辑了,但面试的时候很多人第一反应还是它,说明大家的学习路径还停留在老版本API上。
1.3 从JDK版本看选型的分水岭
这个选型的分水岭非常清晰:JDK 7引入NIO.2,也就是java.nio.file包,之后目录遍历的正确打开方式就从File切换到了Path和Files。
JDK 7之前,你只能用File.listFiles()递归,或者File.list()拿名字再拼接路径,非常别扭。JDK 7之后有了Files.walkFileTree和DirectoryStream;JDK 8又给Files.walk加了Stream版本,配合Lambda写起来就非常清爽了。
所以我的建议很简单:如果你的项目还停留在JDK 6,那你只能老老实实写File递归;只要你在JDK 8及以上,优先用NIO的方式。这里没有情怀可讲,就是技术路线的问题。而且从后来的坑来看,老API在权限问题、符号链接、超大目录这些边界情况下,处理能力都明显弱于NIO,越早切换越省心。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 经典File API递归遍历:自己动手写一个可用版本
2.1 一个完整的递归遍历模板
虽然我说了现在不推荐用File递归做核心逻辑,但你必须会写,因为它是理解其他所有方案的基础。面试的时候面试官也常会让你先讲这个,再看你知不知道更好的替代方案。
我给你一个可以实际跑的模板,这个模板我在早期项目里用了很久,用来扫描配置目录并收集所有.properties文件路径:
java复制import java.io.File;
import java.util.ArrayList;
import java.util.List;
public class FileTraversalDemo {
public static List<File> listAllFiles(File root) {
List<File> result = new ArrayList<>();
collectFiles(root, result);
return result;
}
private static void collectFiles(File dir, List<File> result) {
if (dir == null || !dir.exists()) {
return;
}
File[] children = dir.listFiles();
if (children == null) {
// 目录无权限或IO异常时,listFiles返回null
return;
}
for (File child : children) {
if (child.isDirectory()) {
collectFiles(child, result);
} else {
result.add(child);
}
}
}
public static void main(String[] args) {
File root = new File(".");
List<File> files = listAllFiles(root);
for (File file : files) {
System.out.println(file.getAbsolutePath());
}
}
}
这段代码的逻辑很直接:如果是目录就递归进去,如果是文件就收集。children == null的判断一定不能省,我第一次写就栽在这里。File.listFiles()在目录不存在或者没有读权限时返回的不是空数组,而是null,直接for循环就会空指针。
2.2 过滤、剪枝与结果收集:关键细节不能省
上面的模板只是基础版,真实场景里你肯定要加过滤和剪枝。比如我只想找.log文件,同时要跳过target目录和.git目录。直接在递归里加判断就行:
java复制private static void collectFiles(File dir, List<File> result) {
File[] children = dir.listFiles();
if (children == null) {
return;
}
for (File child : children) {
if (child.isDirectory()) {
// 剪枝:跳过不需要进入的子目录
String name = child.getName();
if (".git".equals(name) || "target".equals(name) || "node_modules".equals(name)) {
continue;
}
collectFiles(child, result);
} else {
// 过滤:只收集.log文件,且文件大小大于0
if (child.getName().endsWith(".log") && child.length() > 0) {
result.add(child);
}
}
}
}
这里有个很容易被忽略的细节:File.isDirectory()在文件不存在时返回false,但性能上它其实隐含了一次系统调用。如果你在遍历大目录时发现比较慢,可以考虑先拿listFiles()返回的File[]数组里的类型判断,不过File这个类没有暴露类型位,所以你很难完全避免多次stat。
另一个细节是文件名的分隔符。老代码里很多人喜欢自己拼parent + "/" + name,这在Windows上就埋雷了。正确做法永远是new File(parent, name)或者更推荐直接用Paths.get(parent, name),让API自己去处理分隔符。我见过太多因为手写/导致Windows下路径错乱的惨案。
还有排序。listFiles()返回的顺序是不保证的,在Unix文件系统上它一般是目录项的顺序,不是字典序。如果业务上要求输出稳定,比如生成文件清单需要对比差异,那你必须在收集完后排序,或者用支持排序的集合接收。这个问题在本地环境不容易暴露,一上测试环境就变“玄学”,因为不同文件系统的目录遍历顺序不一样。
2.3 File API的局限决定了它的适用位置
File递归最大的问题有三个。
第一:访问失败没有容错通道。listFiles()要么返回文件数组,要么返回null,到底是“目录不存在”还是“没权限”还是“IO错误”,你完全不知道。在大目录里碰到个别无权限的子目录,只能选择整体中断或者悄悄跳过,不可能做到精确的失败记录。
第二:符号链接没有保护。如果你用File递归去遍历一个包含符号链接指向父目录的结构,它不会自动识别,很容易无限递归下去直接栈溢出。你只能自己在代码里判断File.getCanonicalPath()和File.getAbsolutePath()是否一致,或者限制递归深度,非常麻烦。
第三:内存与句柄压力。每一层递归都会创建一个新的File[]数组,递归层级深了栈帧也多,层级浅但文件多的目录会一次性加载整个目录项数组到内存。相比DirectoryStream那种按需迭代的方式,确实比较浪费。
那File递归还适合什么场景?我现在的答案是:适合目录层级浅、文件量小、对异常不敏感的内部工具脚本,或者说写作业、应付简单面试的时候。生产逻辑我基本不用它。
3. NIO时代的标配:Files.walk与walkFileTree实战
3.1 Files.walk:用Stream风格遍历目录,附完整示例
JDK 8引入Stream之后,遍历目录这件事的代码量直接降了一个量级。Files.walk返回一个Stream<Path>,你可以用Stream的各种方法去过滤、映射、收集。最典型的用法就是收集目录下所有文件路径:
java复制import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class WalkDemo {
public static List<Path> listAllFiles(String rootDir) throws IOException {
try (Stream<Path> stream = Files.walk(Paths.get(rootDir))) {
return stream.filter(Files::isRegularFile)
.collect(Collectors.toList());
}
}
public static void main(String[] args) throws IOException {
List<Path> files = listAllFiles(".");
files.forEach(System.out::println);
}
}
注意我用了try-with-resources包住Stream。这一步非常关键,Files.walk返回的Stream底层持有目录的文件句柄或类似的系统资源,不关闭的话在Windows上会直接导致文件被占用,在Linux上文件描述符也会慢慢泄漏。我见过同事在循环里反复调用Files.walk但不关流,跑了几万次之后进程直接“Too many open files”挂掉。如果你是在一个长周期应用里做定时扫描,这个问题尤其致命。
Files.walk有两个重载,一个是无限深度,另一个是限制深度:
java复制// 最多往下走3层
Files.walk(Paths.get(rootDir), 3)
限制深度这个参数非常适合做“只扫两层目录”之类的需求,比自己在递归里记深度要简洁得多。它会在达到最大深度后不再进入子目录,但会列出当前层的所有条目。
3.2 Files.walkFileTree:回调模型为什么更省心
Files.walkFileTree是我在生产环境用得最多、也最推荐的方式。它把遍历过程拆成了事件,让你在各个节点上挂逻辑。核心是FileVisitor接口,四个方法的语义我一个个讲清楚:
| 方法 | 调用时机 | 典型用途 | 返回值的意义 |
|---|---|---|---|
preVisitDirectory |
进入目录前 | 记录目录名、判断是否跳过 | CONTINUE继续进入,SKIP_SUBTREE跳过这个目录的全部子内容 |
visitFile |
遇到文件时 | 收集文件、统计大小、处理文件 | CONTINUE继续,TERMINATE终止整个遍历 |
visitFileFailed |
文件或目录访问失败时 | 记录失败原因、决定是否继续 | 默认实现会抛异常终止遍历,重写后返回CONTINUE可以跳过 |
postVisitDirectory |
目录所有子内容处理完后 | 统计该目录汇总信息、删除空目录 | CONTINUE继续遍历兄弟目录 |
实际写代码几乎不需要直接实现接口,继承SimpleFileVisitor按需覆写方法就行。来看一个完整的例子:统计目录下所有文件总大小,同时跳过target目录,并且访问失败不中断。
java复制import java.io.IOException;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.concurrent.atomic.AtomicLong;
public class WalkFileTreeDemo {
public static long calculateSize(String rootDir) throws IOException {
AtomicLong total = new AtomicLong();
Path root = Paths.get(rootDir);
Files.walkFileTree(root, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) {
if (dir.endsWith("target") || dir.endsWith(".git")) {
return FileVisitResult.SKIP_SUBTREE;
}
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
total.addAndGet(attrs.size());
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException {
System.err.println("访问失败: " + file + " -> " + exc.getMessage());
return FileVisitResult.CONTINUE;
}
});
return total.get();
}
public static void main(String[] args) throws IOException {
long size = calculateSize(".");
System.out.println("总大小: " + size + " bytes");
}
}
这里有几个值得说的点。
第一个是preVisitDirectory里用dir.endsWith("target")判断。Path.endsWith比较的是路径片段,不会因为/home/app/target和D:\project\target这种绝对路径差异而误判,它只判断最后一段是不是target。如果你的项目里既有frontend/target又有backend/target,这段逻辑能一次跳过所有名为target的目录,非常高效。
第二个是visitFileFailed的默认行为。很多人不知道,SimpleFileVisitor里visitFileFailed的默认实现是重新抛出exc,也就是说默认情况下,遇到任何一个没有权限的文件,整个遍历会直接中断。真实场景里你会碰到各种乱七八糟的访问失败,比如Linux下/root目录只对root用户开放,Windows下某个文件被其他进程锁定。覆写成返回CONTINUE是基本操作,生产环境我还会把失败路径记录到一个日志列表里,方便事后分析。
第三个是attrs.size()。用BasicFileAttributes拿文件大小比file.toFile().length()快,因为它伴随遍历过程一起返回,不需要额外的stat系统调用。统计大目录时,这个差异非常明显。
3.3 按扩展名搜索与目录剪枝:两种实用场景
说两个我实际用的扩展场景,拿过来就能改。第一个是按扩展名搜索文件:
java复制public static List<Path> findByExtension(Path root, String extension) throws IOException {
List<Path> result = new ArrayList<>();
Files.walkFileTree(root, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
if (file.getFileName().toString().endsWith(extension)) {
result.add(file);
}
return FileVisitResult.CONTINUE;
}
});
return result;
}
第二个是深度限制的遍历,比如只扫两层。用Files.walk加maxDepth参数也能实现,但walkFileTree里没有直接的深度参数,需要自己在preVisitDirectory里控制层级。我是这么做的:
java复制Files.walkFileTree(root, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) {
int depth = root.relativize(dir).getNameCount();
if (depth >= 2) {
return FileVisitResult.SKIP_SUBTREE;
}
return FileVisitResult.CONTINUE;
}
});
这里root.relativize(dir).getNameCount()算的是当前目录相对于根目录的层级数。根目录本身层级是0,第一层子目录是1,第二层是2。到了第二层就SKIP_SUBTREE,正好做到“只扫两层”。这个写法比在递归里传level参数要干净,而且对Stream版本也适用。
3.4 关于符号链接:默认不跟随才是好事
符号链接是文件遍历里最容易出问题的地方。Files.walk和Files.walkFileTree默认是不跟随符号链接的,这意味着如果你有一个link指向某个目录,遍历时link会作为文件出现,但不会进入它指向的目录。
这个默认行为说实话是很安全的,因为它天然避免了循环。但如果你确实需要跟随符号链接,可以给两个方法都传FileVisitOption.FOLLOW_LINKS:
java复制Files.walkFileTree(root, EnumSet.of(FileVisitOption.FOLLOW_LINKS), Integer.MAX_VALUE, visitor);
这个参数一旦开启,你就要想着符号链接循环的事了。比如linkA -> /这种,跟随之后你的遍历会把整个根文件系统都扫一遍。比较稳妥的做法是配合preVisitDirectory检查是不是符号链接,如果是就跳过:
java复制@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) {
if (Files.isSymbolicLink(dir)) {
return FileVisitResult.SKIP_SUBTREE;
}
return FileVisitResult.CONTINUE;
}
提示:使用
Files.isSymbolicLink判断符号链接,需要在preVisitDirectory阶段拦截。因为一旦进入目录,访问的还是实际目录内容,你已经分不清它是从哪个链接进来的了。
4. 性能、边界与选型:不同方式到底差在哪
4.1 不同方式在性能上的真实差距
先说明,我没法给你一个放之四海而皆准的基准测试数字,因为目录结构、文件系统、磁盘类型影响太大。但从我自己的经验来看,有几个趋势是稳定的。
File.listFiles加递归,在几千几万个文件的规模下,性能其实和Files.walk差距不大,省心程度差很多。真正的差距在几十万、上百万文件时体现出来。我做过一个索引工具,扫描一个包含约80万个小文件的目录树,用File递归堆内存使用非常高,因为每个目录都会创建一个File[]数组,而且递归层数深一点栈帧压力也大。后来换成了walkFileTree,配合SimpleFileVisitor在回调里直接处理,内存占用大幅下降,整个过程也稳定很多。
Files.walk和Files.walkFileTree在纯遍历速度上差别不太大,它们底层共用同一套遍历机制。真正的区别在你在遍历过程中做了什么。Files.walk是Stream模型,你要收集到List再处理,中间集合会占用内存;walkFileTree是回调模型,每个文件访问到就直接处理,不需要存中间结果,内存优势明显。
如果你只需要“遍历时看一眼”而不需要收集结果,比如统计数量、统计大小、判断是否存在满足条件的文件,walkFileTree是唯一合理的选择。
4.2 并行遍历的诱惑与风险
Files.walk返回的Stream可以轻易地调用.parallel()并行化,初看很香。但我要泼一盆冷水:文件系统遍历通常不吃并行这一套。
机械硬盘本身是单磁头寻道设备,并发读取同一个目录不仅不会变快,反而会因为磁头来回移动而变慢。固态硬盘虽然随机读能力强,但如果你的文件操作是CPU密集型的,比如对每个文件做哈希、压缩,并行才有意义;如果只是attrs.size()这种轻量操作,并行带来的提升微乎其微,还多了一堆线程上下文切换的开销。
更麻烦的是,并行Stream在遇到异常时处理起来更别扭,而且如果你在遍历中修改共享集合,还得考虑线程安全。我的习惯是:默认串行,除非已经明确存在性能瓶颈,且瓶颈确实在CPU处理环节,再去做并行优化。写并行代码前先Profile,不要靠猜。
4.3 我的选型建议:按场景对号入座
给一个我自己现在写代码用的判断标准,你也可以直接套:
- 单层目录、几十个文件、临时脚本:
Files.list()就够了,连递归都不用。 - 深层目录、文件量中等、只要收集路径列表:
Files.walk()配Stream,代码最简洁。 - 深层目录、文件量大、需要容错与剪枝:
Files.walkFileTree()配SimpleFileVisitor,没有悬念。 - 需要删除整个目录树:
walkFileTree,在visitFile里删文件,在postVisitDirectory里删目录,这是Java官方推荐的递归删除方案。 - 老项目、JDK 6、不能升级:
File递归,但自己补好空指针、权限和符号链接处理。
判断标准就一条:你需要在遍历过程中做什么级别的操作,以及你能不能在失败时继续往下走。 能回答清楚这两点,选型基本不会错。
5. 排坑实录:遍历目录最常见的五个问题
5.1 权限不足把遍历中断了
这个问题在Linux系统上特别常见。默认的SimpleFileVisitor里,visitFileFailed会把异常往上抛,导致整个遍历终止。你以为代码写对了,结果放到生产环境一跑,日志里一个AccessDeniedException,后面全没执行。
解决方案就是我上面讲的,覆写visitFileFailed返回CONTINUE。但要注意,CONTINUE是继续遍历同级的其他分支,不是跳过当前目录继续进入。如果失败的是目录,那这个目录下的所有子目录和文件也不会被遍历了,这是合理的,因为访问失败本身就意味着进不去。
5.2 路径分隔符与相对路径的锅
在Windows上,路径分隔符是反斜杠,在Linux和macOS上是正斜杠。如果你写死了"/"拼接路径,Windows上大概率出问题。用Path.resolve()或者Paths.get()去拼接,永远不会错。
另一个坑是相对路径。Path对象在没有toAbsolutePath()之前,getNameCount()这些方法只操作相对路径的部分。我在做深度限制时遇到过:Paths.get(".")相对路径下,relativize结果里带着.,nameCount算出来不对。处理办法是进入遍历前先把根路径转成绝对路径:
java复制Path root = Paths.get(".").toAbsolutePath().normalize();
这个小小的转换能省掉很多诡异的边界问题。
5.3 符号链接循环:目录树里最隐蔽的炸弹
前面提到过,开启FOLLOW_LINKS后,符号链接可能带你绕一圈回到原位。最经典的是目录里有一个指向自身父目录的链接,遍历会在一层层目录间无限循环,要么栈溢出,要么文件句柄耗尽。
我的排查经验是:先关掉FOLLOW_LINKS跑一遍,如果问题消失,那就是符号链接的锅。然后针对个别确实需要跟随的目录,单独处理并加深度限制。不要图省事全局开启FOLLOW_LINKS,收益和风险完全不成正比。
5.4 大量文件导致内存溢出
如果你用Files.walk收集百万级文件路径到List,内存大概是这样的:每个Path对象及其底层字符串开销,粗算下来至少几十字节。一百万条路径轻松吃掉上百MB堆内存。如果你的应用还有别的使用场景,OOM就跑不掉。
两种解决思路:一是用walkFileTree的回调方式,处理完一个丢一个,不攒中间集合;二是如果必须收集,考虑分批写入磁盘或数据库,不要全留在内存里。我自己的索引工具就是边遍历边写SQLite,内存占用一直很平稳。
5.5 文件在遍历过程中被删除或改名
分布式系统里,文件系统不是静态的。你可能在遍历某个目录时,恰好有另一个进程删除了一个文件。这时候walkFileTree会进入visitFileFailed,你返不返回CONTINUE都可以,但要想清楚业务上怎么处理这种“遍历结果与实际文件系统不一致”的情况。
我的做法是:把遍历产生的快照当作一个时间点视图,不要在半路去校验文件是否存在。如果需要一致性,就在遍历前对整个目录加锁,或者接受一定的最终一致。这个取舍是业务层面的事,不是技术层面能彻底解决的。
6. 面试视角:目录遍历的高频考点和答题思路
6.1 面试官为什么爱问目录遍历
目录遍历在面试里出现频率极高,因为它是典型的“看着简单、深挖很难”的题。面试官可以从一个基础问题一路追问:遍历方式有哪些、递归和迭代的差别、怎么处理符号链接、怎么处理权限异常、大量文件怎么办、怎么删除非空目录。
这些问题背后考察的是三件事:第一,你熟不熟悉File和NIO两代API;第二,你有没有真实处理过文件系统的边界情况;第三,你有没有架构层面的意识,比如内存占用、并发、容错。很多人能写出来File.listFiles递归,但被问到“如果目录是符号链接怎么办”或者“遇到没有权限的子目录你会怎么处理”就卡住了,这说明只是背过API,没有实际做过。
6.2 简洁答题模板:按这三层说不会被扣分
如果你在面试中被问到“用Java遍历目录下所有文件怎么做”,我建议你按三层回答,既显得系统,又不容易漏点。
第一层:基础方案。提一下File.listFiles()递归,给出核心逻辑,顺便说出它的缺陷:listFiles返回null的坑、无法控制失败策略、符号链接可能无限递归。
第二层:现代方案。引出NIO的Files.walk(),说明Stream的好处,代码简洁、支持过滤与深度控制,但强调Stream必须关闭,否则句柄泄漏。
第三层:生产级方案。推荐Files.walkFileTree()加SimpleFileVisitor,详细说明preVisitDirectory剪枝、visitFileFailed容错、attrs.size()高效拿元数据。最后补一句:删除目录树也推荐用walkFileTree,在visitFile删文件、postVisitDirectory删目录。
这样答下来,面试官基本会认为你是有真实经验的。如果你还能顺手举例说明“我在一个80万文件的目录扫描场景里,用walkFileTree替换了File递归,内存和稳定性都明显改善”,那就更有说服力了。
最后再分享一个小技巧。目录遍历的代码看起来简单,但每次我都要反复检查三件事:Stream有没有关、visitFileFailed有没有重写、符号链接有没有考虑。这三件事各踩过一次坑之后,我再也没在目录遍历这个题目上翻过车。你写代码的时候也可以把这三条当成检查清单,跑生产之前过一遍,能省下不少半夜捞日志的时间。
