Java 中如何操作文件

更新于 2025-12-27

MarcoBehler 2020-12-09

你可以使用本指南学习如何通过 Java 的 Path API 操作文件,包括读写文件、监听目录以及使用内存文件系统。


Java 的文件 API

Java 提供了两种文件 API:

  • 原始的 java.io.File API,自 Java 1.0(1996 年)起可用。
  • 较新的 java.nio.file.Path API,自 Java 1.7(2011 年)起可用。

File API 与 Path API 有何区别?

旧的 File API 在大量老项目、框架和库中被广泛使用。尽管它年代久远,但并未被弃用(将来也不太可能被弃用),你仍然可以在任何最新版 Java 中使用它。

然而,java.nio.file.Path 能完成 java.io.File 所能做的所有事情,并且通常做得更好,还提供了更多功能。例如:

  • 文件特性支持:新类支持符号链接、完整的文件属性和元数据(如 PosixFileAttributes)、ACL 等。
  • 更好的使用体验:例如删除文件时,你会收到带有明确错误信息的异常(如“文件不存在”、“文件被锁定”等),而不是一个简单的布尔值 false
  • 解耦设计:支持内存文件系统(我们稍后会讲到)。

(有关两个 API 差异的完整列表,请参阅这篇文章:Oracle 官方文档 - NIO 文件 API

我该使用哪个文件 API?

基于上述原因,如果你正在启动一个新的 Java 项目,强烈建议使用 Paths API 而不是 File API。(虽然 file 读起来确实比 path 更顺口,不是吗?)

因此,本文将只关注 Paths API


Paths API

要在 Java 中操作文件,首先你需要一个对文件的引用(这并不意外!)。正如上面提到的,从 Java 7 开始,你应该使用 Paths API 来引用文件,一切从构建 Path 对象开始。

来看一些代码:

public static void main(String[] args) throws URISyntaxException {
    // Java 11+:使用 Path.of()
    Path path = Path.of("c:\\dev\\licenses\\windows\\readme.txt");
    System.out.println(path);

    path = Path.of("c:/dev/licenses/windows/readme.txt");
    System.out.println(path);

    path = Path.of("c:", "dev", "licenses", "windows", "readme.txt");
    System.out.println(path);

    path = Path.of("c:", "dev", "licenses", "windows").resolve("readme.txt"); // resolve == getChild()
    System.out.println(path);

    path = Path.of(new URI("file:///c:/dev/licenses/windows/readme.txt"));
    System.out.println(path);

    // Java < 11 的等效写法:Paths.get()
    path = Paths.get("c:/dev/licenses/windows/readme.txt");
    System.out.println(path);
}

逐行解析

Path path = Path.of("c:\\dev\\licenses\\windows\\readme.txt");
System.out.println(path);

path = Path.of("c:/dev/licenses/windows/readme.txt");
System.out.println(path);

从 Java 11 开始,应使用静态方法 Path.of 构造路径(稍后我们会介绍 Java 7–10 的等效方式)。

无论你在 Windows 上使用正斜杠 / 还是反斜杠 \Path API 都足够智能,能够根据操作系统自动构造正确的路径。

因此,以上两行在 Windows 上运行时都会输出:

c:\dev\licenses\windows\readme.txt
c:\dev\licenses\windows\readme.txt

你还可以有更多构造路径的方式,不必把完整路径写成一个长字符串:

path = Path.of("c:", "dev", "licenses", "windows", "readme.txt");
System.out.println(path);

path = Path.of("c:", "dev", "licenses", "windows").resolve("readme.txt");
System.out.println(path);

你可以传入多个字符串参数给 Path.of,或者先构造父目录,再用 .resolve(child) 获取子文件(相当于 getChild())。

输出结果仍相同:

c:\dev\licenses\windows\readme.txt
c:\dev\licenses\windows\readme.txt

最后,你甚至可以传入 URI:

path = Path.of(new URI("file:///c:/dev/licenses/windows/readme.txt"));
System.out.println(path);

输出依然不变:

c:\dev\licenses\windows\readme.txt

重要提示

  1. 构造 Path 对象或调用 resolve 并不意味着文件或目录实际存在Path 只是对潜在文件的引用,你需要单独验证其是否存在。
  2. Java 11 之前Path.of 被称为 Paths.get。如果你使用的是较旧的 Java 版本或需要向后兼容,就用 Paths.get。从 Java 11 起,Paths.get 内部其实调用了 Path.of
// Java < 11 等效写法
path = Paths.get("c:/dev/licenses/windows/readme.txt");

一旦你有了 Path 对象,就可以对它进行各种操作了。下一节我们将介绍具体能做什么。


常见文件操作

操作文件或路径时,你很可能会用到 java.nio.file.Files 类。它包含大量用于操作文件和目录的静态方法。

本节可作为快速备忘清单,标题已足够清晰。

如何检查文件是否存在

Path path = Path.of("c:\\dev\\licenses\\windows\\readme.txt");
boolean exists = Files.exists(path);
System.out.println("exists = " + exists);

检查文件或目录是否存在。还可指定额外参数来控制是否跟随符号链接(默认跟随)。

输出示例:

exists = true

如何获取文件的最后修改时间

Path path = Path.of("c:\\dev\\licenses\\windows\\readme.txt");
FileTime lastModifiedTime = Files.getLastModifiedTime(path);
System.out.println("lastModifiedTime = " + lastModifiedTime);

返回 FileTime 对象:

lastModifiedTime = 2020-05-20T08:41:30.905176Z

如何比较两个文件(Java 12+)

Path path = Path.of("c:\\dev\\licenses\\windows\\readme.txt");
long mismatchIndex = Files.mismatch(path, Paths.get("c:\\dev\\whatever.txt"));
System.out.println("mismatch = " + mismatchIndex);

Java 12 新增功能:比较两个文件的字节,返回第一个不匹配字节的位置;若完全相同则返回 -1L

若两个文件完全不同,输出可能是:

mismatch = 0

如何获取文件所有者

Path path = Path.of("c:\\dev\\licenses\\windows\\readme.txt");
UserPrincipal owner = Files.getOwner(path);
System.out.println("owner = " + owner);

返回 UserPrincipal(继承自 Principal)。在 Windows 上会是 WindowsUserPrincipal,包含用户名和 SID:

owner = DESKTOP-168M0IF\marco_local (User)

如何创建临时文件

Path tempFile1 = Files.createTempFile("somePrefixOrNull", ".jpg");
System.out.println("tempFile1 = " + tempFile1);

Path tempFile2 = Files.createTempFile(path.getParent(), "somePrefixOrNull", ".jpg");
System.out.println("tempFile2 = " + tempFile2);

Path tmpDirectory = Files.createTempDirectory("prefix");
System.out.println("tmpDirectory = " + tmpDirectory);

说明:

  • createTempFile(prefix, suffix):前缀可为 null,后缀即扩展名(若省略,默认为 .tmp)。
  • 文件默认创建在系统的临时目录(如 C:\Users\...\AppData\Local\Temp)。
  • 第二种方式允许指定自定义目录。
  • createTempDirectory(prefix) 创建临时目录。

输出示例:

tempFile1 = C:\Users\marco\AppData\Local\Temp\somePrefixOrNull8747488053128491901.jpg
tempFile2 = c:\dev\licenses\windows\somePrefixOrNull11086918945318459411.jpg
tmpDirectory = C:\Users\marco\AppData\Local\Temp\prefix9583768274092262832

⚠️ 注意:临时文件不会自动删除!你必须显式删除它们,尤其是在单元测试或生产环境中。

如何创建普通文件和目录

Path newDirectory = Files.createDirectories(path.getParent().resolve("some/new/dir"));
System.out.println("newDirectory = " + newDirectory);

Path newFile = Files.createFile(newDirectory.resolve("emptyFile.txt"));
System.out.println("newFile = " + newFile);

注意:.resolve() 不会创建文件,只是返回一个指向(子)文件的引用。

输出示例:

newDirectory = c:\dev\licenses\windows\some\new\dir
newFile = c:\dev\licenses\windows\some\new\dir\emptyFile.txt

如何获取 POSIX 权限(仅限 Unix-like 系统)

Path path = Path.of("/home/user/file.txt");
try {
    Set<PosixFilePermission> permissions = Files.getPosixFilePermissions(path);
    System.out.println("permissions = " + permissions);
} catch (UnsupportedOperationException e) {
    System.err.println("看起来你不在 POSIX 文件系统上运行");
}

在 Linux 或 macOS 上输出类似:

[OWNER_WRITE, OWNER_READ, GROUP_WRITE, OTHERS_READ, ...]

写入与读取文件

如何将字符串写入文件

Path utfFile = Files.createTempFile("some", ".txt");
Files.writeString(utfFile, "this is my string ää öö üü"); // 默认 UTF-8
System.out.println("utfFile = " + utfFile);

Path iso88591File = Files.createTempFile("some", ".txt");
Files.writeString(iso88591File, "this is my string ää öö üü", StandardCharsets.ISO_8859_1);
System.out.println("iso88591File = " + iso88591File);

从 Java 11(确切说是 11.0.2 / 12.0,因早期版本有 bug)开始,推荐使用 Files.writeString

如何将字节数组写入文件

Path anotherIso88591File = Files.createTempFile("some", ".txt");
Files.write(anotherIso88591File, "this is my string ää öö üü".getBytes(StandardCharsets.ISO_8859_1));

在 Java 11 之前,这是写入字符串的唯一方式。

写入文件时的选项

Files.writeString(anotherUtf8File, "content", StandardCharsets.UTF_8,
    StandardOpenOption.CREATE,
    StandardOpenOption.TRUNCATE_EXISTING,
    StandardOpenOption.WRITE);

默认行为:自动创建文件,若已存在则截断
若不想覆盖,可传入其他 OpenOption(如 CREATE_NEW 会在文件存在时抛异常)。

使用 Writer 和 OutputStream

try (BufferedWriter writer = Files.newBufferedWriter(utfFile)) {
    // 使用 writer
}

try (OutputStream os = Files.newOutputStream(utfFile)) {
    // 使用输出流
}

务必使用 Files 提供的方法创建流,而非手动构造。

如何从文件读取字符串

String s = Files.readString(utfFile); // 默认 UTF-8
s = Files.readString(utfFile, StandardCharsets.ISO_8859_1);

Java 11+ 推荐使用 Files.readString

如何读取字节数组

byte[] bytes = Files.readAllBytes(utfFile);
String s = new String(bytes, StandardCharsets.UTF_8);

Java 11 之前这是读取字符串的常用方式。

使用 Reader 和 InputStream

try (BufferedReader reader = Files.newBufferedReader(utfFile)) {
    // 使用 reader
}

try (InputStream is = Files.newInputStream(utfFile)) {
    // 使用输入流
}

📌 温馨提醒:文件编码

务必显式指定编码!虽然 Java 11 的新方法默认使用 UTF-8(而非平台默认编码),但仍需注意编码一致性。


移动、删除与列出文件

如何移动文件

// ❌ 错误:不能直接移动到目录
Files.move(utfFile, Path.of("c:\\dev")); // 抛异常!

// ✅ 正确:目标必须包含完整文件名
Files.move(utfFile, Path.of("c:\\dev").resolve(utfFile.getFileName()));

移动选项

// 覆盖已存在的文件
Files.move(src, dst, StandardCopyOption.REPLACE_EXISTING);

// 原子移动(保证完整性)
Files.move(src, dst, StandardCopyOption.ATOMIC_MOVE);

如何删除文件

try {
    Files.delete(tmpDir); // 仅当目录为空时成功
} catch (DirectoryNotEmptyException e) {
    e.printStackTrace();
}

⚠️ Files.delete 无法删除非空目录

如何删除非空目录

try (Stream<Path> walk = Files.walk(tmpDir)) {
    walk.sorted(Comparator.reverseOrder())
        .forEach(path -> {
            try {
                Files.delete(path);
            } catch (IOException e) {
                e.printStackTrace();
            }
        });
}

原理:深度优先遍历,倒序删除(先删子文件,再删父目录)。

如何列出目录中的文件(非递归)

// 列出所有文件
try (var files = Files.list(tmpDirectory)) {
    files.forEach(System.out::println);
}

// 使用 glob 模式(如 *.txt)
try (var files = Files.newDirectoryStream(tmpDirectory, "*.txt")) {
    files.forEach(System.out::println);
}

⚠️ 必须用 try-with-resources 关闭流,否则在 Windows 上会锁住目录!

如何递归列出所有文件

try (var files = Files.walk(tmpDirectory)) {
    files.forEach(System.out::println);
}

同样需要关闭流。


绝对路径、相对路径与规范路径

相对路径

Path p = Paths.get("./src/main/java/../resources/some.properties");
System.out.println("p.isAbsolute() = " + p.isAbsolute()); // false

绝对路径

Path p2 = p.toAbsolutePath();
// 输出:C:\dev\java-file-article\.\src\main\java\..\resources\some.properties
// 注意:仍包含 . 和 ..

规范路径(标准化)

Path p3 = p.normalize().toAbsolutePath();
// 输出:C:\dev\java-file-article\src\main\resources\some.properties

这就是所谓的“规范路径”。

转为相对路径

Path base = Paths.get("C:\\dev\\java-file-article\\");
Path relativized = base.relativize(p3);
// 输出:src\main\resources\some.properties

监听文件与目录

Java WatchService(Java 7+)

低级别 API,依赖原生文件事件(Windows/Linux 支持,macOS 回退到轮询)。

WatchService watcher = FileSystems.getDefault().newWatchService();
Path dir = Path.of("c:\\someDir\\");
dir.register(watcher, ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY);

for (;;) {
    WatchKey key = watcher.take();
    for (WatchEvent<?> event : key.pollEvents()) {
        Path filename = ((WatchEvent<Path>) event).context();
        Path changedFile = dir.resolve(filename);
        // 处理文件
    }
    if (!key.reset()) break;
}

⚠️ 注意事项:

  • 一次保存可能触发多个事件(内容修改 + 时间戳更新)。
  • 编辑器(如 Notepad++)可能通过临时文件间接保存,导致多次事件。
  • 建议加入去抖逻辑(如 Thread.sleep(100) 后再处理)。

Apache Commons IO

更简单的 API,但基于轮询(定期扫描目录):

FileAlterationObserver observer = new FileAlterationObserver(folder);
FileAlterationMonitor monitor = new FileAlterationMonitor(5000); // 5秒轮询
observer.addListener(new FileAlterationListenerAdaptor() {
    @Override
    public void onFileCreate(File file) {
        // 处理新建文件
    }
});
monitor.start();

缺点:仅支持 java.io.File,不支持 Path


内存文件系统

无需真实磁盘 I/O,非常适合测试。

Memory File System

try (FileSystem fs = MemoryFileSystemBuilder.newMacOs().build()) {
    Path file = fs.getPath("/somefile.txt");
    Files.writeString(file, "Hello World");
    System.out.println(Files.readString(file));
}

JimFS(Google)

try (FileSystem fs = Jimfs.newFileSystem(Configuration.unix())) {
    Path file = fs.getPath("/tmp/somefile.txt");
    Files.writeString(file, "Hello World");
    System.out.println(Files.readString(file));
}

关键点

不要使用 Path.of()Paths.get()
因为它们内部调用的是 FileSystems.getDefault(),即真实文件系统。

正确做法:始终通过 fileSystem.getPath(...) 获取路径。


结语

现在你应该掌握了:

  • 基本文件操作(读、写、列、移、删)
  • 路径类型(相对/绝对/规范)
  • 目录监听
  • 内存文件系统用于测试