文档

Java™ 教程
隐藏目录
遍历文件树
教程:基本Java类
课程:基本I/O
章节:文件I/O(包含NIO.2)

遍历文件树

您是否需要创建一个递归访问文件树中所有文件的应用程序?也许您需要删除树中的每个 .class 文件,或者找到最近一年没有被访问的每个文件。您可以使用 FileVisitor 接口来实现。

本节内容包括以下内容:

FileVisitor 接口

要遍历文件树,您首先需要实现一个 FileVisitor。FileVisitor 指定了在遍历过程的关键点上所需的行为:当访问文件时,在访问目录之前,在访问目录之后,或者当出现错误时。该接口有四个方法对应于这些情况:

如果您不需要实现所有四个 FileVisitor 方法,而是要扩展 SimpleFileVisitor 类来代替实现 FileVisitor 接口。这个类实现了 FileVisitor 接口,遍历树中的所有文件,并在遇到错误时抛出 IOError。您可以扩展此类并仅覆盖您需要的方法。

这是一个示例,它扩展了SimpleFileVisitor,以打印文件树中的所有条目。无论条目是普通文件、符号链接、目录还是其他"未指定"类型的文件,它都会打印出来。它还会打印出每个文件的字节大小。遇到的任何异常都会打印到控制台。

FileVisitor方法以粗体显示:

import static java.nio.file.FileVisitResult.*;

public static class PrintFiles
    extends SimpleFileVisitor<Path> {

    // 打印有关每种类型文件的信息。
    @Override
    public FileVisitResult visitFile(Path file,
                                   BasicFileAttributes attr) {
        if (attr.isSymbolicLink()) {
            System.out.format("符号链接: %s ", file);
        } else if (attr.isRegularFile()) {
            System.out.format("普通文件: %s ", file);
        } else {
            System.out.format("其他: %s ", file);
        }
        System.out.println("(" + attr.size() + "字节)");
        return CONTINUE;
    }

    // 打印每个访问过的目录。
    @Override
    public FileVisitResult postVisitDirectory(Path dir,
                                          IOException exc) {
        System.out.format("目录: %s%n", dir);
        return CONTINUE;
    }

    // 如果访问文件时发生错误,让用户知道。
    // 如果你不重写这个方法并且出现错误,将会抛出IOException异常。
    @Override
    public FileVisitResult visitFileFailed(Path file,
                                       IOException exc) {
        System.err.println(exc);
        return CONTINUE;
    }
}

启动过程

当你实现了自己的FileVisitor之后,如何开始文件遍历呢?在Files类中有两个walkFileTree方法。

第一个方法只需要一个起始点和一个FileVisitor实例。你可以按如下方式调用PrintFiles文件访问者:

Path startingDir = ...;
PrintFiles pf = new PrintFiles();
Files.walkFileTree(startingDir, pf);

第二个walkFileTree方法还可以额外指定要访问的层级数限制和一组FileVisitOption枚举值。如果你想确保该方法遍历整个文件树,可以为最大深度参数指定Integer.MAX_VALUE

您可以指定FileVisitOption枚举中的FOLLOW_LINKS,表示应该跟随符号链接。

以下代码片段显示了如何调用四个参数的方法:

import static java.nio.file.FileVisitResult.*;

Path startingDir = ...;

EnumSet<FileVisitOption> opts = EnumSet.of(FOLLOW_LINKS);

Finder finder = new Finder(pattern);
Files.walkFileTree(startingDir, opts, Integer.MAX_VALUE, finder);

创建FileVisitor时的考虑事项

文件树以深度优先方式遍历,但是不能对子目录的访问顺序做任何假设。

如果您的程序将改变文件系统,您需要仔细考虑如何实现您的FileVisitor

例如,如果您正在编写递归删除,您首先删除目录中的文件,然后再删除目录本身。在这种情况下,您将在postVisitDirectory中删除目录。

如果您正在编写递归复制,您在preVisitDirectory中创建新目录,然后再尝试将文件复制到该目录中(在visitFiles中)。如果您想保留源目录的属性(类似于UNIX的cp -p命令),您需要在文件被复制后的postVisitDirectory中执行此操作。 Copy示例展示了如何实现这一点。

如果您正在编写文件搜索,您可以在visitFile方法中执行比较。此方法找到符合条件的所有文件,但不会找到目录。如果您想找到文件和目录,您还必须在preVisitDirectorypostVisitDirectory方法中执行比较。 Find示例展示了如何实现这一点。

您需要决定是否要跟随符号链接。例如,如果要删除文件,跟随符号链接可能不可取。如果要复制文件树,可能希望允许。默认情况下,walkFileTree不会跟随符号链接。

visitFile方法用于处理文件。如果您指定了FOLLOW_LINKS选项,并且您的文件树中存在循环链接到父目录的情况,则循环目录将在visitFileFailed方法中报告,并抛出FileSystemLoopException。以下代码片段显示了如何捕获循环链接,并来自于Copy示例:

@Override
public FileVisitResult
    visitFileFailed(Path file,
        IOException exc) {
    if (exc instanceof FileSystemLoopException) {
        System.err.println("检测到循环: " + file);
    } else {
        System.err.format("无法复制:" + " %s: %s%n", file, exc);
    }
    return CONTINUE;
}

只有在程序跟随符号链接时才会出现这种情况。

控制流程

也许您想遍历文件树寻找特定的目录,并在找到时终止进程。也许您想跳过特定的目录。

FileVisitor 方法返回一个 FileVisitResult 值。您可以通过在 FileVisitor 方法中返回的值来中止文件遍历过程或控制是否访问目录:

在此代码片段中,任何名为 SCCS 的目录都将被跳过:

import static java.nio.file.FileVisitResult.*;

public FileVisitResult
     preVisitDirectory(Path dir,
         BasicFileAttributes attrs) {
    if (dir.getFileName().toString().equals("SCCS")) {
         return SKIP_SUBTREE;
    }
    return CONTINUE;
}

在此代码片段中,一旦找到特定文件,文件名将被打印到标准输出,并且文件遍历将终止:

import static java.nio.file.FileVisitResult.*;

// 我们要查找的文件。
Path lookingFor = ...;

public FileVisitResult
    visitFile(Path file,
        BasicFileAttributes attr) {
    if (file.getFileName().equals(lookingFor)) {
        System.out.println("找到文件:" + file);
        return TERMINATE;
    }
    return CONTINUE;
}

示例

以下示例演示了文件遍历机制:


上一页:链接,符号或其他
下一页:查找文件