文档

Java™ 教程-Java Tutorials 中文版
遍历文件树
Trail: Essential Classes
Lesson: Basic I/O
Section: File I/O (Featuring NIO.2)

遍历文件树

你是否需要创建一个递归访问文件树中所有文件的应用程序?也许你需要删除树中的每个 .class 文件,或查找去年未访问过的每个文件。你可以使用 FileVisitor 接口执行此操作。

本节包括以下内容:

FileVisitor 接口

要遍历文件树,首先需要实现 FileVisitorFileVisitor 指定遍历过程中关键点所需的行为:访问文件时,访问目录之前,访问目录之后,或发生故障时。该接口有四种方法对应于这些情况:

如果你不需要实现所有四个 FileVisitor 方法,则不需要实现 FileVisitor 接口,可以继承 SimpleFileVisitor 类。此类实现 FileVisitor 接口,访问树中的所有文件,并在遇到错误时抛出 IOError。你可以继承此类并仅覆盖所需的方法。

下面是一个继承 SimpleFileVisitor 的示例,用于打印文件树中的所有条目。它打印条目不管条目是常规文件,符号链接,目录还是其他“未指定”类型的文件。它还会打印每个文件的大小(以字节为单位)。遇到的任何异常都会打印到控制台。

FileVisitor 方法以粗体显示:

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

public static class PrintFiles
    extends SimpleFileVisitor<Path> {

    // Print information about
    // each type of file.
    @Override
    public FileVisitResult visitFile(Path file,
                                   BasicFileAttributes attr) {
        if (attr.isSymbolicLink()) {
            System.out.format("Symbolic link: %s ", file);
        } else if (attr.isRegularFile()) {
            System.out.format("Regular file: %s ", file);
        } else {
            System.out.format("Other: %s ", file);
        }
        System.out.println("(" + attr.size() + "bytes)");
        return CONTINUE;
    }

    // Print each directory visited.
    @Override
    public FileVisitResult postVisitDirectory(Path dir,
                                          IOException exc) {
        System.out.format("Directory: %s%n", dir);
        return CONTINUE;
    }

    // If there is some error accessing
    // the file, let the user know.
    // If you don't override this method
    // and an error occurs, an IOException 
    // is thrown.
    @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("cycle detected: " + file);
    } else {
        System.err.format("Unable to copy:" + " %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.*;

// The file we are looking for.
Path lookingFor = ...;

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

例子

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


Previous page: Links, Symbolic or Otherwise
Next page: Finding Files