链接,符号或其他
如前所述,java.nio.file
包,特别是Path
类是“链接感知”的。每个Path
方法都会检测遇到符号链接时该做什么,或者提供一个选项,使您能够配置遇到符号链接时的行为。
到目前为止的讨论一直是关于符号或软链接,但一些文件系统也支持硬链接。硬链接比符号链接更受限制,具体如下:
- 链接的目标必须存在。
- 通常不允许在目录上创建硬链接。
- 硬链接不允许跨分区或卷。因此,它们不能存在于不同文件系统之间。
- 一个硬链接看起来和行为都像一个普通文件,所以它们可能很难找到。
- 从所有方面来看,硬链接与原始文件是相同的实体。它们具有相同的文件权限、时间戳等。所有属性都是相同的。
由于这些限制,硬链接不像符号链接那样经常使用,但Path
方法与硬链接无缝配合。
有几种方法专门处理链接,并在以下部分中介绍:
- 创建符号链接
- 创建硬链接
- 检测符号链接
- 查找链接的目标
创建符号链接
如果你的文件系统支持,你可以使用createSymbolicLink(Path, Path, FileAttribute)
方法创建一个符号链接。第二个Path
参数表示目标文件或目录,可能存在也可能不存在。以下代码片段创建了一个带有默认权限的符号链接:
Path newLink = ...; Path target = ...; try { Files.createSymbolicLink(newLink, target); } catch (IOException x) { System.err.println(x); } catch (UnsupportedOperationException x) { // Some file systems do not support symbolic links. System.err.println(x); }
FileAttributes
vararg 使您能够指定在创建链接时原子设置的初始文件属性。但是,这个参数是为将来使用而设计的,目前尚未实现。
创建硬链接
你可以使用createLink(Path, Path)
方法创建一个到现有文件的硬(或常规)链接。第二个Path
参数定位现有文件,它必须存在,否则会抛出NoSuchFileException
。以下代码片段展示了如何创建链接:
Path newLink = ...; Path existingFile = ...; try { Files.createLink(newLink, existingFile); } catch (IOException x) { System.err.println(x); } catch (UnsupportedOperationException x) { // Some file systems do not // support adding an existing // file to a directory. System.err.println(x); }
检测符号链接
要确定Path
实例是否是符号链接,可以使用isSymbolicLink(Path)
方法。以下代码片段展示了如何:
Path file = ...; boolean isSymbolicLink = Files.isSymbolicLink(file);
欲了解更多信息,请参阅管理元数据。
查找链接的目标
通过使用readSymbolicLink(Path)
方法,您可以获取符号链接的目标,如下所示:
Path link = ...; try { System.out.format("Target of link" + " '%s' is '%s'%n", link, Files.readSymbolicLink(link)); } catch (IOException x) { System.err.println(x); }
如果Path
不是一个符号链接,该方法会抛出NotLinkException
。
遍历文件树
您是否需要创建一个应用程序,递归访问文件树中的所有文件?也许您需要删除树中的每个.class
文件,或者找到在过去一年中未被访问的每个文件。您可以通过FileVisitor
接口实现这一点。
本节涵盖以下内容:
- FileVisitor 接口
- 启动过程
- 创建 FileVisitor 时的注意事项
- 控制流程
- 示例
FileVisitor 接口
要遍历文件树,首先需要实现一个FileVisitor
。FileVisitor
指定了在遍历过程的关键点上所需的行为:当访问文件时,在访问目录之前,在访问目录之后,或者当发生故障时。该接口有四个方法对应于这些情况:
preVisitDirectory
– 在访问目录条目之前调用。postVisitDirectory
– 在访问目录中的所有条目之后调用。如果遇到任何错误,特定异常将传递给该方法。visitFile
– 在访问文件时调用。文件的BasicFileAttributes
被传递给该方法,或者您可以使用 file attributes 包来读取特定的属性集。例如,您可以选择读取文件的DosFileAttributeView
来确定文件是否设置了“hidden”位。visitFileFailed
– 当无法访问文件时调用。特定异常被传递给该方法。您可以选择是否抛出异常,将其打印到控制台或日志文件等。
如果您不需要实现所有四个FileVisitor
方法,而是扩展SimpleFileVisitor
类,而不是实现FileVisitor
接口。这个类实现了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
方法中执行比较。此方法找到所有符合您条件的文件,但不会找到目录。如果您想要找到文件和目录,您还必须在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("cycle detected: " + file); } else { System.err.format("Unable to copy:" + " %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.*; // 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
– 递归查找符合特定通配符模式的文件和目录。此示例在查找文件中讨论。Chmod
– 递归更改文件树上的权限(仅适用于 POSIX 系统)。Copy
– 递归复制文件树。WatchDir
– 演示了监视目录中已创建、删除或修改的文件的机制。使用-r
选项调用此程序会监视整个树的更改。有关文件通知服务的更多信息,请参见监视目录的更改。
查找文件
如果你曾经使用过 shell 脚本,你很可能使用过模式匹配来定位文件。事实上,你可能已经广泛使用了它。如果你还没有使用过,模式匹配使用特殊字符创建模式,然后文件名可以与该模式进行比较。例如,在大多数 shell 脚本中,星号,*
,匹配任意数量的字符。例如,以下命令列出当前目录中以.html
结尾的所有文件:
% ls *.html
java.nio.file
包为这一有用功能提供了编程支持。每个文件系统实现都提供了一个PathMatcher
。你可以通过在FileSystem
类中使用getPathMatcher(String)
方法来检索文件系统的PathMatcher
。以下代码片段获取默认文件系统的路径匹配器:
String pattern = ...; PathMatcher matcher = FileSystems.getDefault().getPathMatcher("glob:" + pattern);
传递给getPathMatcher
的字符串参数指定语法风格和要匹配的模式。本示例指定了glob语法。如果你不熟悉 glob 语法,请参阅什么是 Glob。
Glob 语法易于使用和灵活,但如果你喜欢,也可以使用正则表达式,或regex语法。有关正则表达式的更多信息,请参阅正则表达式课程。一些文件系统实现可能支持其他语法。
如果你想使用其他形式的基于字符串的模式匹配,你可以创建自己的PathMatcher
类。本页中的示例使用 glob 语法。
一旦你创建了PathMatcher
实例,你就可以准备好根据它匹配文件。PathMatcher
接口有一个方法,matches
,它接受一个Path
参数并返回一个布尔值:它要么匹配模式,要么不匹配。以下代码片段查找以.java
或.class
结尾的文件并将这些文件打印到标准输出:
PathMatcher matcher = FileSystems.getDefault().getPathMatcher("glob:*.{java,class}"); Path filename = ...; if (matcher.matches(filename)) { System.out.println(filename); }
递归模式匹配
搜索与特定模式匹配的文件与遍历文件树密切相关。有多少次你知道一个文件在某处在文件系统上,但在哪里?或者也许你需要找到文件树中具有特定文件扩展名的所有文件。
Find
示例正是如此。Find
类似于 UNIX 的find
实用程序,但功能更简化。你可以扩展这个示例以包含其他功能。例如,find
实用程序支持-prune
标志来排除搜索中的整个子树。你可以通过在preVisitDirectory
方法中返回SKIP_SUBTREE
来实现该功能。要实现-L
选项,即跟随符号链接,你可以使用四个参数的walkFileTree
方法,并传入FOLLOW_LINKS
枚举(但请确保在visitFile
方法中测试循环链接)。
要运行 Find 应用程序,请使用以下格式:
% java Find <path> -name "<glob_pattern>"
模式被放置在引号内,以防止 shell 解释任何通配符。例如:
% java Find . -name "*.html"
这里是Find
示例的源代码:
/** * Sample code that finds files that match the specified glob pattern. * For more information on what constitutes a glob pattern, see * https://docs.oracle.com/javase/tutorial/essential/io/fileOps.html#glob * * The file or directories that match the pattern are printed to * standard out. The number of matches is also printed. * * When executing this application, you must put the glob pattern * in quotes, so the shell will not expand any wild cards: * java Find . -name "*.java" */ import java.io.*; import java.nio.file.*; import java.nio.file.attribute.*; import static java.nio.file.FileVisitResult.*; import static java.nio.file.FileVisitOption.*; import java.util.*; public class Find { public static class Finder extends SimpleFileVisitor<Path> { private final PathMatcher matcher; private int numMatches = 0; Finder(String pattern) { matcher = FileSystems.getDefault() .getPathMatcher("glob:" + pattern); } // Compares the glob pattern against // the file or directory name. void find(Path file) { Path name = file.getFileName(); if (name != null && matcher.matches(name)) { numMatches++; System.out.println(file); } } // Prints the total number of // matches to standard out. void done() { System.out.println("Matched: " + numMatches); } // Invoke the pattern matching // method on each file. @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) { find(file); return CONTINUE; } // Invoke the pattern matching // method on each directory. @Override public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) { find(dir); return CONTINUE; } @Override public FileVisitResult visitFileFailed(Path file, IOException exc) { System.err.println(exc); return CONTINUE; } } static void usage() { System.err.println("java Find <path>" + " -name \"<glob_pattern>\""); System.exit(-1); } public static void main(String[] args) throws IOException { if (args.length < 3 || !args[1].equals("-name")) usage(); Path startingDir = Paths.get(args[0]); String pattern = args[2]; Finder finder = new Finder(pattern); Files.walkFileTree(startingDir, finder); finder.done(); } }
递归遍历文件树的内容在遍历文件树中有详细介绍。
监视目录更改
原文:
docs.oracle.com/javase/tutorial/essential/io/notification.html
你是否曾经发现自己正在编辑一个文件,使用 IDE 或另一个编辑器,并且出现一个对话框通知您文件系统中的一个打开文件已更改并需要重新加载?或者,就像 NetBeans IDE 一样,应用程序悄悄地更新文件而不通知您。以下示例对话框显示了使用免费编辑器jEdit时的通知外观:
jEdit 对话框显示检测到修改的文件
要实现此功能,称为文件更改通知,程序必须能够检测到文件系统上相关目录发生的变化。一种方法是轮询文件系统以查找更改,但这种方法效率低下。它不适用于具有数百个打开文件或目录需要监视的应用程序。
java.nio.file
包提供了一个文件更改通知 API,称为 Watch Service API。此 API 使您能够向观察服务注册目录(或目录)。在注册时,您告诉服务您感兴趣的事件类型:文件创建、文件删除或文件修改。当服务检测到感兴趣的事件时,它会转发给注册的进程。注册的进程有一个专用于监视其注册事件的线程(或线程池)。当事件发生时,根据需要进行处理。
本节涵盖以下内容:
- 观察服务概述
- 试一试
- 创建 Watch Service 并注册事件
- 处理事件
- 获取文件名
- 何时使用和不使用此 API
Java 中文官方教程 2022 版(九)(2)https://developer.aliyun.com/article/1486343