首页 / Java 入门教程 / NIO.2:Path 与 Files

Java 入门教程

NIO.2:Path 与 Files

本教程共 100 篇 · 第 91 篇 · 更新于 2026-08-05 · 约 5 分钟阅读

JavaJava 入门教程NIO.2PathFiles文件API

本节目标:掌握现代 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

新项目一律用 PathFiles。老 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);
    }
}

在 Windows 上输出:

notes.txt
docs\java\io.md
D:\workspace\demo

多参数版本的好处是跨平台。你不用关心分隔符是 / 还是 \,交给 API 处理。

Tip

Java 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());
    }
}

输出:

完整路径:D:\project\src\Main.java
文件名:Main.java
父目录:D:\project\src
根:D:\
元素个数:3
第 0 个元素:project
是否绝对路径:true

有个容易忽略的点:getNameCount() 不算根元素D:\ 是根,不计入名字元素,所以三个元素分别是 projectsrcMain.java

相对路径转绝对路径

toAbsolutePath() 把相对路径补全成绝对路径,基准是当前工作目录:

Path p = Path.of("data.txt");
System.out.println(p.toAbsolutePath());

还有一个 normalize(),用来化简路径中的 ...

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

有个规则要记住:如果参数本身是绝对路径,resolve() 会直接返回参数,忽略前面的基准路径。

relativize() 是「求相对路径」:从 A 走到 B 该怎么走。

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());
    }
}

输出:

全文长度: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);
        }
    }
}
Warning

Files.lines() 返回的 Stream 持有文件句柄,必须用 try-with-resources 包起来。忘了关,文件描述符就泄漏了,跑久了会耗尽系统资源。

还有 Files.newBufferedReader()Files.newBufferedWriter(),返回的就是上一节讲过的缓冲字符流,已经帮你包好了。

常用查询方法

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

两个路径指向同一个文件,但写法不同,equals() 就返回 false。要判断是不是同一个文件,得用 Files.isSameFile(a, b),它会真正去问文件系统。

另外 Path 实现了 Iterable<Path>,可以直接用增强 for 逐段遍历:

for (Path part : Path.of("D:", "project", "src", "Main.java")) {
    System.out.println(part);
}

输出的是 projectsrcMain.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,不是完整元素

想判断扩展名,还是老实用 p.getFileName().toString().endsWith(".txt")

异常信息更明确

前面说老 File 类失败时不给理由,NIO.2 在这方面改进很大。

Files 的方法失败时会抛出具体的异常子类,比如文件不存在抛 NoSuchFileException,目标已存在抛 FileAlreadyExistsException,权限不足抛 AccessDeniedException,非空目录抛 DirectoryNotEmptyException

这些异常都继承自 IOException,所以既可以统一捕获,也可以分别处理:

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 不等于文件一定不存在。