bigywater.tech
Java · 353

Files.walkFileTree:递归遍历的访问者模型

遍历目录树看起来是个一行的事,但回调的先后、跳过语义、符号链接、错误处理和性能差别都藏在细节里。这篇用一棵真实的小树和一棵 5000 项的大树逐项验证。

18 / 24
符号链接默认不跟随与 FOLLOW_LINKS 时访问到的条数(后者遇到指向祖先的环)
4 / 15
SKIP_SIBLINGS 在 preVisitDirectory(a) 返回时的回调条数与完整遍历的条数
≈34 vs ≈55 ms
walkFileTree 与 Files.walk + Files.size 遍历 4000 个文件的耗时

回调顺序与目录顺序

实测 · 本机执行输出
  [order]
        pre  .
        file z.txt
        pre  a
        file a/a1.txt
        file a/a2.txt
        pre  a/deep
        file a/deep/d1.txt
        post a/deep
        post a
        pre  c
        post c
        pre  b
        file b/b1.txt
        post b
        post .
    DirectoryStream 对 root 的顺序:[z.txt, a, c, b];排序后:[a, b, c, z.txt];与排序一致? false
    File.list 的顺序:[z.txt, a, c, b]

FileVisitResult 的语义

实测 · 本机执行输出
  [results]
    [SKIP_SUBTREE at a] 共 9 条:pre:. , file:z.txt , pre:a , pre:c , post:c , pre:b , file:b/b1.txt , post:b , post:.
    [SKIP_SIBLINGS at a/a1.txt] 共 11 条:pre:. , file:z.txt , pre:a , file:a/a1.txt , post:a , pre:c , post:c , pre:b , file:b/b1.txt , post:b , post:.
    [SKIP_SIBLINGS at a (preVisit)] 共 4 条:pre:. , file:z.txt , pre:a , post:.
    [TERMINATE at a/a2.txt] 共 5 条:pre:. , file:z.txt , pre:a , file:a/a1.txt , file:a/a2.txt
    [SKIP_SUBTREE returned from visitFile] 共 15 条:pre:. , file:z.txt , pre:a , file:a/a1.txt , file:a/a2.txt , pre:a/deep , file:a/deep/d1.txt , post:a/deep , post:a , pre:c , post:c , pre:b , file:b/b1.txt , post:b , post:.

maxDepth

实测 · 本机执行输出
  [depth]
    maxDepth=0          共  1 条;其中被当作 file 的目录:[.]
    maxDepth=1          共  6 条;其中被当作 file 的目录:[a, c, b]
        pre  .
        file z.txt
        file a (dir)
        file c (dir)
        file b (dir)
        post .
    maxDepth=2          共 13 条;其中被当作 file 的目录:[a/deep]
    maxDepth=3          共 15 条;其中被当作 file 的目录:[]
    maxDepth=MAX_VALUE  共 15 条;其中被当作 file 的目录:[]
    maxDepth=-1 → java.lang.IllegalArgumentException: 'maxDepth' is negative

递归删除

实测 · 本机执行输出
  [delete]
    递归删除的顺序(文件先于所在目录):
        del file z.txt
        del file a/a1.txt
        del file a/a2.txt
        del file a/deep/d1.txt
        del dir  a/deep
        del dir  a
        del dir  c
        del file b/b1.txt
        del dir  b
        del dir  .
    root 是否还存在:false
    Files.delete(非空目录) → DirectoryNotEmptyException: .
    Files.walk + 逆序删除:删了 10 个,root 存在? false

符号链接

实测 · 本机执行输出
  [links]
    (1) 不跟随符号链接(默认):
        pre  .
        file dangling (symlink)
        file z.txt
        pre  a
        file a/a1.txt
        file a/a2.txt
        pre  a/deep
        file a/deep/d1.txt
        post a/deep
        post a
        file link-a (symlink)
        pre  c
        post c
        pre  b
        file b/up (symlink)
        file b/b1.txt
        post b
        post .
    (2) FOLLOW_LINKS:
        pre  .
        file dangling (symlink)
        file z.txt
        pre  a
        file a/a1.txt
        file a/a2.txt
        pre  a/deep
        file a/deep/d1.txt
        post a/deep
        post a
        pre  link-a
        file link-a/a1.txt
        file link-a/a2.txt
        pre  link-a/deep
        file link-a/deep/d1.txt
        post link-a/deep
        post link-a
        pre  c
        post c
        pre  b
        FAIL b/up → FileSystemLoopException
        file b/b1.txt
        post b
        post .
      FOLLOW_LINKS 下共 24 条,不跟随时 18 条

权限错误

实测 · 本机执行输出
  [errors]
    a/deep 的权限设为 --------- 后:
        FAIL a/deep → AccessDeniedException
    默认的 SimpleFileVisitor.visitFileFailed 会重新抛出:AccessDeniedException: <root>/a/deep
    Files.walk(…).count() 抛 UncheckedIOException:java.nio.file.AccessDeniedException: <root>/a/deep
    Files.walk 在抛异常之前已经遍历的:[., z.txt, a, a/a1.txt, a/a2.txt, → UncheckedIOException]
    Files.find 找 *.txt:[a/a1.txt, a/a2.txt, a/deep/d1.txt, b/b1.txt, z.txt]
    Files.walk 权限恢复后:10 个

耗时对比

实测 · 本机执行输出
  [perf]
    构造的树:200 个一级目录,各 5 个子目录,各 4 个文件 → 目录 1201 个,文件 4000 个
    第 4 轮:walkFileTree(读 attrs.size)32.8 ms;Files.walk + isRegularFile + Files.size 51.4 ms;File.listFiles 递归 40.2 ms;Files.walk 只计数 32.3 ms;字节数 4000 / 4000 / 4000,条数 5201
    第 5 轮:walkFileTree(读 attrs.size)34.2 ms;Files.walk + isRegularFile + Files.size 54.8 ms;File.listFiles 递归 42.0 ms;Files.walk 只计数 32.4 ms;字节数 4000 / 4000 / 4000,条数 5201
    (自写计时,macOS APFS,文件已在页缓存里;不是冷缓存)

分步演示

walkFileTree
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / OpenJDK(Temurin 17.0.20、Homebrew 21.0.12 与 27,每篇注明实际用的版本);用 javac 编译、java 直接运行,需要第三方库时用 Maven 解析依赖;计时用 System.nanoTime,数字每次运行会有小幅抖动。JDK 21.0.12,macOS APFS,文件在临时目录;耗时为自写计时循环且文件在页缓存里,不是冷缓存
    • 没有验证:Linux ext4 / XFS 上的目录顺序、冷缓存与网络文件系统(NFS / SMB)上的耗时、Windows 的 junction 与符号链接差异、并发修改目录树(遍历中创建 / 删除文件)的行为、walkFileTree 在深层目录下的栈使用、Files.walk 的并行流用法
    京ICP备2026067502号-1京公网安备11010502063487号