Path 对象代表的是一个文件或目录的路径,它是在不同的操作系统和文件系统之上的抽象。它的目的是,在构建路径时,我们不必注意底层的操作系统,我们的代码不需要重写就能在不同的操作系统上工作。
java.nio.file.Paths 类包含了重载的 static get() 方法,可以接受一个 String 序列,或一个统一资源标识符(Uniform Resource Identifier,URI),然后将其转化为一个 Path 对象:
package file; import java.net.URI; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; public class PathInfo { static void showInfo(Path p) { System.out.println("--------------------"); System.out.println("toString():"+ p); System.out.println("exists():"+ Files.exists(p)); System.out.println("isRegularFile():"+ Files.isRegularFile(p)); System.out.println("isDirectory():"+ Files.isDirectory(p)); System.out.println("isAbsolute():"+ p.isAbsolute()); System.out.println("getFileName():"+ p.getFileName()); System.out.println("getParent():"+ p.getParent()); System.out.println("getRoot():"+ p.getRoot()); } public static void main(String[] args) { System.out.println(System.getProperty("os.name")); Path filePath = Paths.get("src/file/test.txt"); showInfo(filePath); showInfo(filePath.toAbsolutePath()); Path dirPath = Paths.get("src","file"); showInfo(dirPath); Path invalidPath = Paths.get("src","file","invalid.txt"); showInfo(invalidPath); System.out.println("--------------------"); URI fileURI = filePath.toUri(); System.out.println("URI:"+ fileURI); Path fileFromURI = Paths.get(fileURI); System.out.println("Files.exists(fileFromURI):"+ Files.exists(fileFromURI)); /* * Windows 11 * -------------------- * toString(): src\file\test.txt * exists(): true * isRegularFile(): true * isDirectory(): false * isAbsolute(): false * getFileName(): test.txt * getParent(): src\file * getRoot(): null * -------------------- * toString(): D:\IDEA Project\JavaStudy\src\file\test.txt * exists(): true * isRegularFile(): true * isDirectory(): false * isAbsolute(): true * getFileName(): test.txt * getParent(): D:\IDEA Project\JavaStudy\src\file * getRoot(): D:\ * -------------------- * toString(): src\file * exists(): true * isRegularFile(): false * isDirectory(): true * isAbsolute(): false * getFileName(): file * getParent(): src * getRoot(): null * -------------------- * toString(): src\file\invalid.txt * exists(): false * isRegularFile(): false * isDirectory(): false * isAbsolute(): false * getFileName(): invalid.txt * getParent(): src\file * getRoot(): null * -------------------- * URI: file:///D:/IDEA%20Project/JavaStudy/src/file/test.txt * Files.exists(fileFromURI): true */ } }虽然 toString() 生成的是路径的完整表示,但是可以看到 getFileName() 总是会生成文件的名字。使用 Files 工具类(后面会看到更多),我们可以测试文件是否存在,是否为“普通”文件,是否为目录等等。
在这里,我们看到了文件的 URI 是什么样子的,但 URI 可以用来描述大多数事物,不仅限于文件,然后我们成功地将 URI 转回到了一个 Path 之中。
1.1 获取Path的片段 package file; import java.nio.file.Path; import java.nio.file.Paths; public class PartsOfPaths { public static void main(String[] args) { Path p = Paths.get("src/file/test.txt").toAbsolutePath(); System.out.println("Path:"+ p); System.out.println("endsWith(.txt):"+ p.endsWith(".txt")); for (int i = 0; i < p.getNameCount(); i++) System.out.print(p.getName(i) + (i == p.getNameCount() - 1 ?"":"/")); System.out.println(); for (Path pName: p) { System.out.println("p.startsWith("+ pName +"):"+ p.startsWith(pName)); System.out.println("p.endsWith("+ pName +"):"+ p.endsWith(pName)); } System.out.println("p.startsWith("+ p.getRoot() +"):"+ p.startsWith(p.getRoot())); /* * Path: D:\IDEA Project\JavaStudy\src\file\test.txt * endsWith(.txt): false * IDEA Project/JavaStudy/src/file/test.txt * p.startsWith(IDEA Project): false * p.endsWith(IDEA Project): false * p.startsWith(JavaStudy): false * p.endsWith(JavaStudy): false * p.startsWith(src): false * p.endsWith(src): false * p.startsWith(file): false * p.endsWith(file): false * p.startsWith(test.txt): false * p.endsWith(test.txt): true * p.startsWith(D:\): true */ } }在 getNameCount() 界定的上限之内,我们可以结合索引使用 getName(),得到一个 Path 的各个部分。Path 也可以生成 Iterator,所以我们也可以使用 for-in 来遍历。
注意,尽管这里的路径确实是以 .txt 结尾的,但 endsWith() 的结果是 false。这是因为该方法比较的是整个路径组件(即用 / 分开的每个整体部分,如 test.txt),而不是名字中的一个字串。
还可以看到我们在对 Path 进行遍历时,并没有包含根目录,只有当我们用根目录来检查 startsWith() 时才会得到 true。
1.2 获取Path信息Files 工具类中包含了一整套用于检查 Path 的各种信息的方法:
package file; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; public class PathAnalysis { public static void main(String[] args) throws IOException { Path p = Paths.get("src/file/test.txt").toAbsolutePath(); System.out.println("exists():"+ Files.exists(p)); System.out.println("isDirectory():"+ Files.isDirectory(p)); System.out.println("isExecutable():"+ Files.isExecutable(p)); System.out.println("isReadable():"+ Files.isReadable(p)); System.out.println("isRegularFile():"+ Files.isRegularFile(p)); System.out.println("isWritable():"+ Files.isWritable(p)); System.out.println("notExists():"+ Files.notExists(p)); // 以下方法会抛出IOException System.out.println("isHidden():"+ Files.isHidden(p)); System.out.println("size():"+ Files.size(p)); System.out.println("getFileStore():"+ Files.getFileStore(p)); System.out.println("getLastModifiedTime():"+ Files.getLastModifiedTime(p)); System.out.println("getOwner():"+ Files.getOwner(p)); System.out.println("probeContentType():"+ Files.probeContentType(p)); /* * exists(): true * isDirectory(): false * isExecutable(): true * isReadable(): true * isRegularFile(): true * isWritable(): true * notExists(): false * isHidden(): false * size(): 30 * getFileStore(): Asano (D:) * getLastModifiedTime(): 2023-10-20T02:58:25.335851Z * getOwner(): LAPTOP-23NEHV3U\AsanoSaki (User) * probeContentType(): text/plain */ } } 1.3 添加或删除路径片段我们必须能够通过对自己的 Path 对象添加和删除某些路径片段来构建 Path 对象。若想去掉某个基准路径可以使用 relativize(),想在一个 Path 对象的后面添加路径片段可以使用 resolve():
package file; import java.io.IOException; import java.nio.file.Path; import java.nio.file.Paths; public class AddAndSubPaths { public static void main(String[] args) throws IOException { Path base = Paths.get("src").toAbsolutePath(); System.out.println("base:"+ base); Path filePath = Paths.get("src/file/test.txt").toAbsolutePath(); System.out.println("filePath:"+ filePath); Path filePathWithoutBase = base.relativize(filePath); System.out.println("filePathWithoutBase:"+ filePathWithoutBase); Path filePathWithBase = base.resolve("file/test.txt"); System.out.println("filePathWithBase:"+ filePathWithBase); Path workspacePath = Paths.get(""); System.out.println("workspacePath:"+ workspacePath); System.out.println("workspacePath.toRealPath():"+ workspacePath.toRealPath()); /* * base: D:\IDEA Project\JavaStudy\src * filePath: D:\IDEA Project\JavaStudy\src\file\test.txt * filePathWithoutBase: file\test.txt * filePathWithBase: D:\IDEA Project\JavaStudy\src\file\test.txt * workspacePath: * workspacePath.toRealPath(): D:\IDEA Project\JavaStudy */ } }只有 Path 为绝对路径时,才能将其用作 relativize() 方法的参数。此外还增加了对 toRealPath() 的进一步测试,除了路径不存在的情况下会抛出 IOException 异常,它总是会对 Path 进行扩展和规范化。
2. 文件系统为了完整起见,我们需要一种方式来找出文件系统的其他信息。在这里,我们可以使用静态的 FileSystems 工具来获得默认的文件系统,也可以在一个 Path 对象上调用 getFileSystem() 来获得创建这个路径对象的文件系统。我们可以通过给定的 URI 获得一个文件系统,也可以构建一个新的文件系统(如果操作系统支持的话)。
package file; import java.nio.file.FileStore; import java.nio.file.FileSystem; import java.nio.file.FileSystems; import java.nio.file.Path; public class FileSystemDemo { public static void main(String[] args) { FileSystem fs = FileSystems.getDefault(); for (FileStore s: fs.getFileStores()) System.out.println("File Store:"+ s); for (Path p: fs.getRootDirectories()) System.out.println("Root Directory:"+ p); System.out.println("Separator:"+ fs.getSeparator()); System.out.println("isOpen:"+ fs.isOpen()); System.out.println("isReadOnly:"+ fs.isReadOnly()); System.out.println("File Attribute Views:"+ fs.supportedFileAttributeViews()); /* * File Store: OS (C:) * File Store: Asano (D:) * File Store: Saki (E:) * Root Directory: C:\ * Root Directory: D:\ * Root Directory: E:\ * Separator: \ * isOpen: true * isReadOnly: false * File Attribute Views: [owner, dos, acl, basic, user] */ } } 3. 查找文件java.nio.file 中有一个用于查找文件的类 PathMatcher,可以通过在 FileSystem 对象上调用 getPathMatcher() 来获得一个 PathMatcher 对象,并传入我们感兴趣的查找模式。模式有两个选项:glob 和 regex。glob 更简单,但实际上非常强大,可以解决很多问题,如果问题更为复杂,可以使用 regex。
package file; import java.io.IOException; import java.nio.file.*; public class FindFiles { public static void main(String[] args) throws IOException { Path filePath = Paths.get("src/file"); PathMatcher matcher1 = FileSystems.getDefault().getPathMatcher("glob:**/*.{txt,tmp}"); Files.walk(filePath) // 会抛出IOException .filter(matcher1::matches) .forEach(System.out::println); System.out.println("--------------------"); PathMatcher matcher2 = FileSystems.getDefault().getPathMatcher("glob:*.txt"); Files.walk(filePath) .map(Path::getFileName) // 不加这行代码将不会产生任何输出 .filter(matcher2::matches) .forEach(System.out::println); /* * src\file\FileToWordsBuilder.txt * src\file\test.txt * -------------------- * FileToWordsBuilder.txt * test.txt */ } }在 matcher1 中,glob 表达式开头的 **/ 表示所有子目录,如果你想匹配的不仅仅是以基准目录为结尾的 Path,那么它是必不可少的,因为它匹配的是完整路径,直到找到想要的结果。单个的 * 表示任何东西,后面的花括号表示的是一系列的可能性,即查找任何以 .txt 或 .tmp 结尾的东西。
注意在 matcher2 中我们只匹配 *.txt,而我们的 filePath 并不在基准目录下,因此需要先用 map() 将文件的完整路径改为只剩最后一段 Path 片段,即 xxx.txt 这种形式,这样才能匹配上。
4. 读写文件目前为止,我们可以做的只是对路径和目录的操作,现在来看看如何操作文件本身的内容。
java.nio.file.Files 类包含了方便读写文本文件和二进制文件的工具函数。Files.readAllLines() 可以一次性读入整个文件,生成一个 List
Files.write() 也被重载了,可以将 byte 数组或任何实现了 Iterable 接口的类的对象写入文件:
package file; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Random; public class FilesWrite { static Random rand = new Random(47); public static void main(String[] args) throws IOException { byte[] bytes = new byte[10]; rand.nextBytes(bytes); Path bytesPath = Paths.get("src/file/bytes.txt"); Files.write(bytesPath, bytes); // 会抛出IOException for (byte b: Files.readAllBytes(bytesPath)) System.out.print(b +""); System.out.println(); System.out.println("The size of bytes.txt:"+ Files.size(bytesPath)); List现在我们读取文件时都是将文件全部一次性读入,这可能会有以下的问题:
这个文件非常大,如果一次性读取整个文件,可能会耗尽内存。我们只需要文件的一部分就能得到想要的结果,所以读取整个文件是在浪费时间。Files.lines() 可以很方便地将一个文件变为一个由行组成的 Stream:
package file; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Paths; public class ReadLineStream { public static void main(String[] args) throws IOException { Files.lines(Paths.get("src/file/contents.txt")) // 会抛出IOException .skip(1) .map(String::toUpperCase) .forEach(System.out::println); /* * HELLO JAVA * HELLO ASANOSAKI */ } }如果把文件当作一个由行组成的输入流来处理,那么 Files.lines() 非常有用,但是如果我们想在一个流中完成读取、处理和写入,那该怎么办呢?
package file; import java.io.IOException; import java.io.PrintWriter; import java.nio.file.Files; import java.nio.file.Paths; import java.util.stream.Stream; public class StreamInAndOut { public static void main(String[] args) { try (Stream因为我们是在同一个块中执行的所有操作,所以两个文件可以在相同的 try-with-resources 块中打开。PrintWriter 是一个旧式的 java.io 类,允许我们“打印”到—个文件,所以它是这个应用的理想选择。