这些Java教程是针对JDK 8编写的。本页面中描述的示例和实践不利用后续版本引入的改进,并可能使用不再可用的技术。
有关Java SE 9及其后续版本中更新的语言特性的概述,请参阅Java语言变化。
有关所有JDK版本的新功能、增强功能以及已删除或弃用选项的信息,请参阅JDK发布说明。
您是否需要创建一个递归访问文件树中所有文件的应用程序?也许您需要删除树中的每个 .class 文件,或者找到最近一年没有被访问的每个文件。您可以使用 FileVisitor 接口来实现。
本节内容包括以下内容:
要遍历文件树,您首先需要实现一个 FileVisitor。FileVisitor 指定了在遍历过程的关键点上所需的行为:当访问文件时,在访问目录之前,在访问目录之后,或者当出现错误时。该接口有四个方法对应于这些情况:
preVisitDirectory – 在访问目录的条目之前调用。postVisitDirectory – 在访问目录的所有条目之后调用。如果遇到任何错误,具体的异常将传递给该方法。visitFile – 在访问的文件上调用。文件的 BasicFileAttributes 会传递给该方法,或者您可以使用 文件属性 包来读取一组特定的属性。例如,您可以选择读取文件的 DosFileAttributeView 来确定文件是否设置了 "hidden" 标志位。visitFileFailed – 当无法访问文件时调用。具体的异常将传递给该方法。您可以选择是抛出异常、将其打印到控制台或日志文件中等。如果您不需要实现所有四个 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。
例如,如果您正在编写递归删除,您首先删除目录中的文件,然后再删除目录本身。在这种情况下,您将在postVisitDirectory中删除目录。
如果您正在编写递归复制,您在preVisitDirectory中创建新目录,然后再尝试将文件复制到该目录中(在visitFiles中)。如果您想保留源目录的属性(类似于UNIX的cp -p命令),您需要在文件被复制后的postVisitDirectory中执行此操作。 示例展示了如何实现这一点。Copy
如果您正在编写文件搜索,您可以在visitFile方法中执行比较。此方法找到符合条件的所有文件,但不会找到目录。如果您想找到文件和目录,您还必须在preVisitDirectory或postVisitDirectory方法中执行比较。 示例展示了如何实现这一点。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 方法中返回的值来中止文件遍历过程或控制是否访问目录:
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.*;
// 我们要查找的文件。
Path lookingFor = ...;
public FileVisitResult
visitFile(Path file,
BasicFileAttributes attr) {
if (file.getFileName().equals(lookingFor)) {
System.out.println("找到文件:" + file);
return TERMINATE;
}
return CONTINUE;
}
以下示例演示了文件遍历机制: