Path / Files / FileSystem 文件遍历源码
概述
NIO.2(JDK 7 引入的 java.nio.file 包)把文件系统抽象成三层:Path(路径对象,跨平台表示)、FileSystem(文件系统实例,file: 对应默认文件系统)、Files(静态工具方法,全部委托给 FileSystemProvider 执行)。相比 java.io.File,它支持符号链接、目录流、文件树遍历、监听器与自定义文件系统。
核心设计是SPI 扩展:FileSystemProvider 通过 ServiceLoader 加载,默认注册 file:、jrt:、zip: 等实现,第三方可注册 Jimfs(内存文件系统)之类的自定义文件系统。本文基于 OpenJDK 21 源码拆解路径创建、规范化、遍历与 SPI 加载链路。
核心源码解析
① Paths.get(String first, String... more) 的路径创建
java
public final class Paths {
public static Path get(String first, String... more) {
return FileSystems.getDefault().getPath(first, more); // ① 委托默认文件系统
}
}FileSystems.getDefault()返回全局单例:FileSystemProvider中名为file的 provider 首次加载后缓存。WindowsFileSystem.getPath(first, more)把变参用分隔符拼接成字符串,再包成WindowsPath;Linux 对应UnixPath。路径对象只是字符串封装,不触发任何系统调用。Path是接口,WindowsPath/UnixPath各自实现平台相关的解析逻辑。
② WindowsPath 的规范化
java
// WindowsPath
@Override
public Path normalize() {
final int count = nameCount();
if (count == 0 || getName(0).toString().length() == 0) {
return this; // 空路径直接返回
}
...
removeDots(); // ① 移除 . 与 ..
...
}
@Override
public Path toRealPath(LinkOption... options) throws IOException {
checkRead();
try {
String path = GetFinalPathNameByHandle(open(), options); // ② Windows native
return new WindowsPath(path, this);
} catch (IOException ioe) {
...
}
}normalize()的语义:移除多余的.段,把a/./b折叠成a/b、a/x/../b折叠成a/b;它只做字符串处理,不访问文件系统,所以不要求路径真实存在。removeDots()内部用两个索引(skipPath与removeCount)扫描段数组,遇到.跳过、遇到..回退上一段,O(n) 一次遍历完成。toRealPath()才真正触碰文件系统:先open()用CreateFile打开目标,再GetFinalPathNameByHandle拿到解析符号链接后的真实绝对路径,最后把结果重新包成WindowsPath。toAbsolutePath()则纯内存拼接当前工作目录,不访问磁盘。
③ Path.resolve(Path other) / relativize(Path other)
java
// WindowsPath / UnixPath 共用逻辑
public Path resolve(Path other) {
Path otherPath = checkPath(other); // 必须是同文件系统的 Path
if (otherPath.isAbsolute()) return other; // ① 绝对路径直接返回
if (otherPath.isEmpty()) return this; // ② 空路径返回自身
switch (otherPath.getNameCount()) {
case 0: return this;
default:
String base = asString();
if (!base.endsWith("/")) base += "/";
return new WindowsPath(base + otherPath, this.fs); // ③ 拼接
}
}
public Path relativize(Path other) {
...
// 逐段比较,找到公共前缀之后计算 .. 数量
for (int i = 0; i < mn; i++) {
if (getName(i).equals(other.getName(i))) {
n--;
m--;
} else {
break;
}
}
...
}resolve是拼接语义:/a/b.resolve("c/d")得到/a/b/c/d;若参数是绝对路径则直接覆盖。relativize是取相对语义:先统计双方公共前缀段数,剩余段中当前路径的段变成..,目标路径剩余段直接拼接,得到从自身到目标的相对路径。两者互为逆运算(对同一父路径)。- 平台差异:Windows 上大小写不敏感比较,且要处理盘符(
C:与D:之间无法 relativize,抛IllegalArgumentException)。
④ Files.walkFileTree(Path start, FileVisitor visitor) 的递归遍历
java
public static Path walkFileTree(Path start, Set<FileVisitOption> options,
int maxDepth, FileVisitor<? super Path> visitor)
throws IOException {
try {
FileTreeWalker walker = new FileTreeWalker(options, maxDepth);
FileTreeWalker.Event ev = walker.walk(start); // ① 逐事件驱动
do {
FileVisitResult result;
switch (ev.type()) {
case ENTRY: // ② 普通文件/目录
IOException ioe = ev.ioeException();
result = (ioe == null)
? visitor.visitFile(ev.file(), ev.attributes())
: visitor.visitFileFailed(ev.file(), ioe);
break;
case START_DIRECTORY: // ③ 进入目录前
result = visitor.preVisitDirectory(ev.file(), ev.attributes());
if (result == FileVisitResult.SKIP_SUBTREE) {
walker.skipRemainingSiblings();
break;
}
...
case END_DIRECTORY: // ④ 目录遍历完
result = visitor.postVisitDirectory(ev.file(), ev.ioeException());
break;
}
...
ev = walker.next(); // ⑤ 取下一个事件
} while (ev != null);
} finally {
walker.close();
}
return start;
}FileTreeWalker内部维护显式栈(List<DirectoryStream<Path>>)实现深度优先,不递归调用,避免深目录树栈溢出。- 每个事件驱动
visitor对应回调:visitFile/preVisitDirectory/postVisitDirectory/visitFileFailed;返回值FileVisitResult控制流程(CONTINUE/SKIP_SUBTREE/SKIP_SIBLINGS/TERMINATE)。 - 目录遍历用
newDirectoryStream(dir)惰性打开,walker.next()每次取目录流中的下一个条目。
⑤ Files.walk(Path maxDepth) 的 Stream<Path>
java
public static Stream<Path> walk(Path start, int maxDepth, FileVisitOption... options)
throws IOException {
FileTreeIterator iterator = new FileTreeIterator(start, maxDepth, options); // ① 迭代器
try {
return StreamSupport.stream(Spliterators.spliteratorUnknownSize(iterator,
Spliterator.DISTINCT | Spliterator.NONNULL), false) // ② 惰性流
.onClose(iterator::close); // ③ 关闭钩子
} catch (Error | RuntimeException e) {
iterator.close();
throw e;
}
}FileTreeIterator复用FileTreeWalker,把Event序列包装成Iterator<Path>。StreamSupport.stream生成的流是惰性的:终端操作触发时才开始真正遍历,iterator.next()内部一次只取一个事件。- 流关闭时自动关闭所有已打开的目录流(
iterator::close),防止文件描述符泄漏;若未消费完就关闭,遍历立即停止。
⑥ Files.newDirectoryStream(Path dir, String glob) 的 glob 过滤
java
public static DirectoryStream<Path> newDirectoryStream(Path dir, String glob)
throws IOException {
if (glob == null) throw new NullPointerException("'glob' is null");
final PathMatcher matcher = dir.getFileSystem().getPathMatcher("glob:" + glob); // ① 编译 glob
return dir.getFileSystem().provider().newDirectoryStream(dir,
entry -> matcher.matches(entry.getFileName())); // ② 过滤谓词
}getPathMatcher("glob:*.java")返回Globs编译的正则匹配器:glob 语法(*、?、[abc]、{a,b})被翻译成等价的正则表达式后编译为Pattern。- Windows 实现
WindowsDirectoryStream构造时用FindFirstFileExW打开目录句柄,iterator().next()时用FindNextFileW读取下一个条目,命中过滤谓词才返回。 - 该流同样惰性且必须
close();DirectoryStream只允许一次迭代,iterator()只能调用一次。
⑦ FileSystemProvider.installedProviders() 的 SPI 加载
java
public static List<FileSystemProvider> installedProviders() {
if (installedProviders == null) {
FileSystemProvider defaultProvider = new WindowsFileSystemProvider(); // ① file 默认优先
List<FileSystemProvider> providers = new ArrayList<>();
ServiceLoader<FileSystemProvider> sl = ServiceLoader
.load(FileSystemProvider.class, ClassLoader.getSystemClassLoader()); // ② SPI
for (FileSystemProvider provider : sl) {
...
providers.add(provider);
}
providers.add(defaultProvider); // ③ file 放在最后兜底
installedProviders = List.copyOf(providers);
}
return installedProviders;
}- 顺序规则:默认
file:provider 强制放在列表末尾,其余按ServiceLoader的声明顺序在前——这样FileSystems.getDefault()总能在列表中优先匹配到file实现。 - 内置 provider 位于
jdk.zipfs(zip:)与jdk.internal.jrtfs(jrt:)模块,通过模块声明provides FileSystemProvider with ...暴露给ServiceLoader。 FileSystemProvider还提供内存中注册的静态 API:installedProviders()之后的运行时 provider 可通过FileSystemProvider.installProvider动态加入,无需重启 JVM。
⑧ FileSystems.newFileSystem(URI uri, Map env) 的自定义文件系统
java
public static FileSystem newFileSystem(URI uri, Map<String, ?> env) throws IOException {
return getFileSystemProvider(uri).newFileSystem(uri, env); // ① 按 scheme 找 provider
}
private static FileSystemProvider getFileSystemProvider(URI uri) {
String scheme = uri.getScheme(); // ② uri 的协议
for (FileSystemProvider provider : installedProviders()) {
if (scheme.equalsIgnoreCase(provider.getScheme())) { // ③ 匹配 scheme
return provider;
}
}
throw new FileSystemNotFoundException("Provider \"" + scheme + "\" not found");
}zip:协议由jdk.zipfs模块的ZipFileSystemProvider处理:newFileSystem(URI, env)打开 ZIP/JAR 文件并解析条目表,把整个压缩包虚拟成一个只读/可写的文件系统。- 第三方示例
Jimfs注册jimfs:scheme,把文件树存在内存 Map 中,提供接近真实文件系统的语义(权限、软链接、toRealPath等)。 FileSystems.getDefault()本质是provider("file").getFileSystem(URI.create("file:///"))——一切文件访问最终都收敛到 provider 这一层,这就是 NIO.2 可扩展性的根。