# 走文件树

> 原文： [https://docs.oracle.com/javase/tutorial/essential/io/walk.html](https://docs.oracle.com/javase/tutorial/essential/io/walk.html)

您是否需要创建一个递归访问文件树中所有文件的应用程序？也许您需要删除树中的每个`.class`文件，或者查找去年未访问过的每个文件。您可以使用 [`FileVisitor`](https://docs.oracle.com/javase/8/docs/api/java/nio/file/FileVisitor.html) 接口执行此操作。

本节包括以下内容：

*   [FileVisitor 接口](#filevisitor)
*   [启动流程](#invoke)
*   [创建 FileVisitor 时的注意事项](#order)
*   [控制流量](#return)
*   [例子](#ex)

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

*   [`preVisitDirectory`](https://docs.oracle.com/javase/8/docs/api/java/nio/file/FileVisitor.html#preVisitDirectory-T-java.nio.file.attribute.BasicFileAttributes-) - 在访问目录条目之前调用。
*   [`postVisitDirectory`](https://docs.oracle.com/javase/8/docs/api/java/nio/file/FileVisitor.html#postVisitDirectory-T-java.io.IOException-) - 访问目录中的所有条目后调用。如果遇到任何错误，则将特定异常传递给该方法。
*   [`visitFile`](https://docs.oracle.com/javase/8/docs/api/java/nio/file/FileVisitor.html#visitFile-T-java.nio.file.attribute.BasicFileAttributes-) - 在被访问的文件上调用。文件的`BasicFileAttributes`传递给方法，或者您可以使用[文件属性](fileAttr.html)包来读取一组特定的属性。例如，您可以选择读取文件的`DosFileAttributeView`以确定文件是否设置了“隐藏”位。
*   [`visitFileFailed`](https://docs.oracle.com/javase/8/docs/api/java/nio/file/FileVisitor.html#visitFileFailedy-T-java.io.IOException-) - 无法访问文件时调用。特定异常传递给该方法。您可以选择是抛出异常，将其打印到控制台还是日志文件，等等。

如果您不需要实现所有四种`FileVisitor`方法，而不是实现`FileVisitor`接口，则可以扩展 [`SimpleFileVisitor`](https://docs.oracle.com/javase/8/docs/api/java/nio/file/SimpleFileVisitor.html) 类。该类实现`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`方法。

*   [`walkFileTree(Path, FileVisitor)`](https://docs.oracle.com/javase/8/docs/api/java/nio/file/Files.html#walkFileTree-java.nio.file.Path-java.nio.file.FileVisitor-)
*   [`walkFileTree(Path, Set&lt;FileVisitOption&gt;, int, FileVisitor)`](https://docs.oracle.com/javase/8/docs/api/java/nio/file/Files.html#walkFileTree-java.nio.file.Path-java.util.Set-int-java.nio.file.FileVisitor-)

第一种方法只需要`FileVisitor`的起点和实例。您可以按如下方式调用`PrintFiles`文件访问者：

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

```

第二个`walkFileTree`方法使您可以另外指定访问的级别数和一组 [`FileVisitOption`](https://docs.oracle.com/javase/8/docs/api/java/nio/file/FileVisitOption.html) 枚举的限制。如果要确保此方法遍历整个文件树，可以为最大深度参数指定`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`。

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

如果您正在编写递归副本，则在尝试将文件复制到`visitFiles`之前，在`preVisitDirectory`中创建新目录（在`visitFiles`中）。如果要保留源目录的属性（类似于 UNIX `cp -p`命令），则需要在复制文件后在`postVisitDirectory`中执行 _。 [``Copy`` ](examples/Copy.java)示例显示了如何执行此操作。_

如果您正在编写文件搜索，则在`visitFile`方法中执行比较。此方法查找符合条件的所有文件，但找不到目录。如果要查找文件和目录，还必须在`preVisitDirectory`或`postVisitDirectory`方法中执行比较。 [``Find`` ](examples/Find.java)示例显示了如何执行此操作。

您需要决定是否要遵循符号链接。例如，如果要删除文件，则可能不建议使用符号链接。如果要复制文件树，则可能需要允许它。默认情况下，`walkFileTree`不遵循符号链接。

为文件调用`visitFile`方法。如果已指定`FOLLOW_LINKS`选项且文件树具有指向父目录的循环链接，则使用`FileSystemLoopException`在`visitFileFailed`方法中报告循环目录。以下代码片段显示了如何捕获循环链接，并且来自 [``Copy``](examples/Copy.java)示例：

```
@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`](https://docs.oracle.com/javase/8/docs/api/java/nio/file/FileVisitResult.html) 值。您可以中止文件遍历过程或控制在`FileVisitor`方法中返回的值是否访问目录：

*   `CONTINUE` - 表示文件行走应继续。如果`preVisitDirectory`方法返回`CONTINUE`，则访问该目录。
*   `TERMINATE` - 立即中止文件行走。返回此值后，不会调用其他文件遍历方法。
*   `SKIP_SUBTREE` - 当`preVisitDirectory`返回此值时，将跳过指定的目录及其子目录。这个分支被“修剪”出树。
*   `SKIP_SIBLINGS` - 当`preVisitDirectory`返回此值时，不访问指定的目录，不调用`postVisitDirectory`，也不再访问未访问的兄弟姐妹。如果从`postVisitDirectory`方法返回，则不会访问其他兄弟姐妹。基本上，在指定的目录中没有进一步发生。

在此代码段中，将跳过名为`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;
}

```

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

*   [``Find`` ](examples/Find.java)- 递归文件树，查找与特定 glob 模式匹配的文件和目录。该示例在[查找文件](find.html)中讨论。
*   [``Chmod`` ](examples/Chmod.java)- 递归更改文件树的权限（仅适用于 POSIX 系统）。
*   [``Copy`` ](examples/Copy.java)- 递归复制文件树。
*   [``WatchDir`` ](examples/WatchDir.java)- 演示为目录查看已创建，删除或修改的文件的机制。使用`-r`选项调用此程序会监视整个树的更改。有关文件通知服务的更多信息，请参阅[查看目录以进行更改](notification.html)。