NIO.2:Path 与 Files
本教程共 100 篇 · 第 91 篇 · 更新于 2026-08-05 · 约 5 分钟阅读
91. NIO.2:Path 与 Files
本节目标:掌握现代 Java 的文件 API,用 Path 表示路径、用 Files 一行完成读写。
老 File 类的问题
Java 1.0 就有 java.io.File 类,但它的设计有不少缺陷。
最要命的是失败时不给理由。file.delete() 返回 false,你根本不知道是文件不存在、还是没权限、还是被别的程序占用。
其次是功能缺失。它不支持符号链接、无法读取文件权限、不能监听目录变化、复制文件得自己写循环。
Java 7 引入了一整套新 API,放在 java.nio.file 包下,业内叫它 NIO.2。核心是两个东西:Path 表示路径,Files 提供操作。
Note新项目一律用
Path和Files。老File只在维护旧代码时才会遇到,两者之间可以用file.toPath()和path.toFile()互转。
创建 Path
Path 是一个接口,表示文件系统中的一个路径。注意它只是「路径」这个字符串的结构化表示,创建 Path 对象不会检查文件是否真的存在。
推荐用 Path.of()(Java 11 加入):
import java.nio.file.Path;
public class PathCreate {
public static void main(String[] args) {
Path p1 = Path.of("notes.txt"); // 相对路径
Path p2 = Path.of("docs", "java", "io.md"); // 自动用分隔符拼接
Path p3 = Path.of("D:", "workspace", "demo"); // Windows 绝对路径
System.out.println(p1);
System.out.println(p2);
System.out.println(p3);
}
}
```text
在 Windows 上输出:
```text
notes.txt
docs\java\io.md
D:\workspace\demo
多参数版本的好处是跨平台。你不用关心分隔符是 / 还是 \,交给 API 处理。
TipJava 7 到 10 用的是
Paths.get(...),功能完全一样。Path.of()只是更简洁的新写法,两者可以混用。
拆解路径
Path 把路径看成一串「名字元素」,提供了一组方法来取其中的部分:
import java.nio.file.Path;
public class PathParts {
public static void main(String[] args) {
Path path = Path.of("D:", "project", "src", "Main.java");
System.out.println("完整路径:" + path);
System.out.println("文件名:" + path.getFileName());
System.out.println("父目录:" + path.getParent());
System.out.println("根:" + path.getRoot());
System.out.println("元素个数:" + path.getNameCount());
System.out.println("第 0 个元素:" + path.getName(0));
System.out.println("是否绝对路径:" + path.isAbsolute());
}
}
```text
输出:
```text
完整路径:D:\project\src\Main.java
文件名:Main.java
父目录:D:\project\src
根:D:\
元素个数:3
第 0 个元素:project
是否绝对路径:true
有个容易忽略的点:getNameCount() 不算根元素。D:\ 是根,不计入名字元素,所以三个元素分别是 project、src、Main.java。
相对路径转绝对路径
toAbsolutePath() 把相对路径补全成绝对路径,基准是当前工作目录:
Path p = Path.of("data.txt");
System.out.println(p.toAbsolutePath());
```java
还有一个 `normalize()`,用来化简路径中的 `.` 和 `..`:
```java
Path messy = Path.of("a/b/../c/./d.txt");
System.out.println(messy.normalize()); // a\c\d.txt
toRealPath() 则更彻底:它会解析符号链接、化简路径、转成绝对路径,但要求文件必须真实存在,否则抛 NoSuchFileException。
resolve 与 relativize
这两个方法是一对反操作,很实用。
resolve() 是「拼接」:在当前路径后面接上另一段。
Path base = Path.of("D:", "workspace");
Path full = base.resolve("project/src/App.java");
System.out.println(full); // D:\workspace\project\src\App.java
```java
有个规则要记住:如果参数本身是绝对路径,`resolve()` 会直接返回参数,忽略前面的基准路径。
`relativize()` 是「求相对路径」:从 A 走到 B 该怎么走。
```java
Path a = Path.of("/home/user/docs");
Path b = Path.of("/home/user/pics/cat.png");
System.out.println(a.relativize(b)); // ..\pics\cat.png
两者互为逆运算:a.resolve(a.relativize(b)) 化简后等于 b。
Warning
relativize()要求两个路径必须同为绝对路径或同为相对路径,混着来会抛IllegalArgumentException。
Files 的一行式读写
Files 是个工具类,全是静态方法。它把常见的文件操作压缩成一行,这是 NIO.2 最爽的部分。
读整个文本文件:
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.util.List;
public class FilesReadWrite {
public static void main(String[] args) throws IOException {
Path path = Path.of("demo.txt");
// 一行写出
Files.writeString(path, "第一行\n第二行\n第三行\n", StandardCharsets.UTF_8);
// 一行读成字符串
String content = Files.readString(path, StandardCharsets.UTF_8);
System.out.println("全文长度:" + content.length());
// 一行读成 List
List<String> lines = Files.readAllLines(path, StandardCharsets.UTF_8);
System.out.println("共 " + lines.size() + " 行");
lines.forEach(line -> System.out.println(" " + line));
// 追加内容
Files.writeString(path, "第四行\n", StandardCharsets.UTF_8,
StandardOpenOption.APPEND);
System.out.println("追加后行数:" + Files.readAllLines(path).size());
}
}
```text
输出:
```text
全文长度:12
共 3 行
第一行
第二行
第三行
追加后行数:4
readString() 和 writeString() 是 Java 11 加入的,之前只能用 readAllBytes() 再自己转字符串。
打开选项
Files.write 系列方法的最后一个参数是可变参数 OpenOption,常用的有:
| 选项 | 作用 |
|---|---|
CREATE | 文件不存在则创建 |
CREATE_NEW | 创建新文件,已存在则报错 |
APPEND | 追加到末尾 |
TRUNCATE_EXISTING | 清空已有内容 |
DELETE_ON_CLOSE | 关闭时删除,适合临时文件 |
不传任何选项时,默认是 CREATE + TRUNCATE_EXISTING + WRITE,也就是「有就覆盖,没有就新建」。
处理大文件用流式方法
readAllLines() 会把整个文件装进内存。文件几百 MB 时,这么干会直接 OOM。
正确做法是用 Files.lines(),它返回一个 Stream<String>,边读边处理:
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.util.stream.Stream;
public class BigFileRead {
public static void main(String[] args) throws IOException {
Path path = Path.of("demo.txt");
try (Stream<String> stream = Files.lines(path, StandardCharsets.UTF_8)) {
long count = stream.filter(line -> line.contains("行")).count();
System.out.println("含「行」的行数:" + count);
}
}
}
```bash
> [!WARNING]
> `Files.lines()` 返回的 `Stream` 持有文件句柄,**必须用 try-with-resources 包起来**。忘了关,文件描述符就泄漏了,跑久了会耗尽系统资源。
还有 `Files.newBufferedReader()` 和 `Files.newBufferedWriter()`,返回的就是上一节讲过的缓冲字符流,已经帮你包好了。
## 常用查询方法
```java
Files.exists(path); // 是否存在
Files.notExists(path); // 是否确定不存在
Files.isDirectory(path); // 是否是目录
Files.isRegularFile(path); // 是否是普通文件
Files.isReadable(path); // 是否可读
Files.size(path); // 文件字节数
exists() 和 notExists() 不是简单的取反关系。当程序没有权限访问该路径时,两个方法都会返回 false——因为无法确定。
Path 的比较与遍历
Path 重写了 equals(),但比较的是路径字符串的结构,不是磁盘上的实际文件。
Path a = Path.of("docs/a.txt");
Path b = Path.of("docs/./a.txt");
System.out.println(a.equals(b)); // false
System.out.println(a.equals(b.normalize())); // true
```java
两个路径指向同一个文件,但写法不同,`equals()` 就返回 `false`。要判断是不是同一个文件,得用 `Files.isSameFile(a, b)`,它会真正去问文件系统。
另外 `Path` 实现了 `Iterable<Path>`,可以直接用增强 for 逐段遍历:
```java
for (Path part : Path.of("D:", "project", "src", "Main.java")) {
System.out.println(part);
}
输出的是 project、src、Main.java 三段,根元素不参与遍历,这一点和 getNameCount() 一致。
还有两个判断方法:startsWith() 检查路径前缀,endsWith() 检查路径后缀。注意它们比较的是完整的名字元素,不是字符串片段。
Path p = Path.of("/home/user/report.txt");
System.out.println(p.endsWith("report.txt")); // true
System.out.println(p.endsWith(".txt")); // false,不是完整元素
```bash
想判断扩展名,还是老实用 `p.getFileName().toString().endsWith(".txt")`。
## 异常信息更明确
前面说老 `File` 类失败时不给理由,NIO.2 在这方面改进很大。
`Files` 的方法失败时会抛出具体的异常子类,比如文件不存在抛 `NoSuchFileException`,目标已存在抛 `FileAlreadyExistsException`,权限不足抛 `AccessDeniedException`,非空目录抛 `DirectoryNotEmptyException`。
这些异常都继承自 `IOException`,所以既可以统一捕获,也可以分别处理:
```java
try {
Files.readString(Path.of("missing.txt"));
} catch (NoSuchFileException e) {
System.out.println("文件不存在:" + e.getFile());
} catch (IOException e) {
System.out.println("其他 IO 错误:" + e.getMessage());
}
e.getFile() 直接给出出问题的路径,排查起来省心不少。这种「说清楚为什么失败」的设计,正是新 API 值得切换的重要理由。
本节小结
- NIO.2 用
Path表示路径,Files提供操作,全面取代老File。 Path.of()多参数拼接可跨平台,创建时不检查文件是否存在。getNameCount()不包含根元素,getFileName()取最后一段。resolve()拼接路径,relativize()求相对路径,两者互逆。readString/writeString/readAllLines一行完成读写。- 大文件用
Files.lines()流式处理,且必须 try-with-resources。 exists()返回false不等于文件一定不存在。