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
之中。
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
。
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
*/
}
}
我们必须能够通过对自己的 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
进行扩展和规范化。
为了完整起见,我们需要一种方式来找出文件系统的其他信息。在这里,我们可以使用静态的 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]
*/
}
}
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
这种形式,这样才能匹配上。
目前为止,我们可以做的只是对路径和目录的操作,现在来看看如何操作文件本身的内容。
java.nio.file.Files
类包含了方便读写文本文件和二进制文件的工具函数。Files.readAllLines()
可以一次性读入整个文件,生成一个 List
:
package file;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
public class ReadAllLines {
public static void main(String[] args) throws IOException {
Path p = Paths.get("src/file/test.txt");
List<String> lines = Files.readAllLines(p); // 会抛出IOException
for (String line: lines)
System.out.println(line);
System.out.println("--------------------");
Files.readAllLines(p)
.stream()
.map(line -> line.substring(0, line.length() / 2))
.forEach(System.out::println);
/*
* Hello world
* Java is a nice language
* --------------------
* Hello
* Java is a n
*/
}
}
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<String> contents = new ArrayList<>(Arrays.asList("Hello world", "Hello java", "Hello AsanoSaki"));
Path filePath = Paths.get("src/file/contents.txt");
Files.write(filePath, contents);
Files.readAllLines(filePath).stream().forEach(System.out::println);
System.out.println("The size of contents.txt: " + Files.size(filePath));
/*
* -107 66 36 -70 22 5 91 102 -85 10
* The size of bytes.txt: 10
* Hello world
* Hello java
* Hello AsanoSaki
* The size of contents.txt: 42
*/
}
}
现在我们读取文件时都是将文件全部一次性读入,这可能会有以下的问题:
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<String> in = Files.lines(Paths.get("src/file/contents.txt"));
PrintWriter out = new PrintWriter("src/file/uppercaseContents.txt")) {
in.map(String::toUpperCase).forEachOrdered(out::println);
} catch (IOException e) {
System.out.println("Caught IOException");
}
}
}
因为我们是在同一个块中执行的所有操作,所以两个文件可以在相同的 try-with-resources
块中打开。PrintWriter
是一个旧式的 java.io
类,允许我们“打印”到—个文件,所以它是这个应用的理想选择。