摘要: 本文全面介绍Java NIO.2文件操作API,涵盖从传统I/O到现代NIO.2的升级路径。详细讲解Path接口、Files工具类的核心用法,包括文件创建、读写、复制、移动、删除等基础操作,以及目录遍历、文件属性获取、与旧API互操作等高级功能。通过丰富的代码示例和综合工具类展示,帮助开发者掌握更简洁、高效的Java文件处理方式,提升日常开发效率。
关键词: Java NIO.2, Path接口, Files工具类, 文件操作, 目录遍历
在上一篇文章中,我们学习了 Java I/O 流的基础:字节流、字符流、缓冲流和转换流。你应该已经能用 FileInputStream、BufferedReader 等工具读写文件了。
但你可能也隐隐感觉到:那些 API 用起来有点“笨重”。
比如,要读取一个文本文件的所有行,你需要嵌套多层装饰器:new BufferedReader(new InputStreamReader(new FileInputStream("test.txt"), "UTF-8"))——这一串写起来既冗长又容易出错。而且,古老的 File 类在文件操作上也有很多不足:它只能检查文件是否存在、获取文件名,但复制、移动、遍历目录这些常见操作都需要你自己写循环和递归,非常不方便。
好在 Java 早在 2011 年(JDK 7)就引入了一套全新的文件 I/O API,叫做 NIO.2(New I/O 2.0)。它彻底改变了 Java 处理文件和目录的方式,提供了:
- 更简洁、更直观的 API(Path 接口和 Files 工具类)
- 支持符号链接、文件属性、目录流等高级特性
- 更高效的异步 I/O 能力(本篇不涉及)
今天,我们就来全面学习这套现代化的文件操作 API。以后你写文件操作,几乎可以告别传统的 File 和 FileInputStream 了。
1. 从 File 到 Path —— 更现代的路径表示
在 NIO.2 中,文件和目录的路径不再用 File 类表示,而是用 Path 接口。Path 比 File 更灵活,支持多种文件系统(比如 ZIP 文件系统),并且提供了丰富的方法来操作路径。
1.1 创建 Path 对象
使用 Paths.get() 静态方法:
import java.nio.file.Path;
import java.nio.file.Paths;
public class PathDemo {
public static void main(String[] args) {
// 用字符串创建 Path
Path path1 = Paths.get("/home/user/test.txt");
Path path2 = Paths.get("docs", "readme.md"); // 多参数,自动拼接
Path path3 = Paths.get("."); // 当前目录
System.out.println(path1); // /home/user/test.txt
System.out.println(path2); // docs/readme.md
System.out.println(path3.toAbsolutePath()); // 当前工作目录的绝对路径
}
}
在 Windows 上,路径分隔符是 \\,但你可以在代码里统一使用 /,Paths.get() 会自动适配操作系统。
1.2 Path 的常用方法
Path p = Paths.get("/home/user/projects/demo/config.properties");
System.out.println("文件名:" + p.getFileName()); // config.properties
System.out.println("父路径:" + p.getParent()); // /home/user/projects/demo
System.out.println("根路径:" + p.getRoot()); // /
System.out.println("名称元素个数:" + p.getNameCount()); // 5
System.out.println("第2个元素:" + p.getName(1)); // user
// 拼接路径
Path newPath = p.resolveSibling("backup.properties");
System.out.println("同目录下其他文件:" + newPath); // /home/user/projects/demo/backup.properties
// 规范化(去除 . 和 ..)
Path normalized = Paths.get("/home/../user/./test.txt").normalize();
System.out.println(normalized); // /user/test.txt
Path 的 API 设计得很直观,方法名就能看出用途。
2. Files 工具类 —— 文件操作的“瑞士军刀”
Files 是 NIO.2 的核心工具类,提供了海量的静态方法,用于创建、复制、移动、删除、读取、写入文件,以及操作目录和文件属性。它比传统的 File 强大太多了。
重要:Files 类所有方法在发生 I/O 错误时都会抛出 IOException(受检异常),所以调用时需要用 try-catch 或 throws。
2.1 文件存在性检查和创建
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.io.IOException;
public class FilesCheckDemo {
public static void main(String[] args) throws IOException {
Path path = Paths.get("test.txt");
// 检查文件是否存在
if (Files.exists(path)) {
System.out.println("文件存在");
} else {
System.out.println("文件不存在");
}
// 检查是否为普通文件(不是目录)
boolean isRegular = Files.isRegularFile(path);
// 检查是否为目录
boolean isDir = Files.isDirectory(path);
// 检查是否可读、可写、可执行
boolean readable = Files.isReadable(path);
boolean writable = Files.isWritable(path);
boolean executable = Files.isExecutable(path);
}
}
2.2 创建文件、目录和临时文件
// 创建空文件(如果不存在)
Path file = Paths.get("newfile.txt");
Files.createFile(file); // 如果文件已存在会抛出 FileAlreadyExistsException
// 创建目录(单层)
Path dir = Paths.get("mydir");
Files.createDirectory(dir); // 如果父目录不存在或目录已存在会抛异常
// 创建多级目录(父目录不存在时自动创建)
Path deepDir = Paths.get("a/b/c/d");
Files.createDirectories(deepDir); // 推荐使用这个
// 创建临时文件/目录
Path tempFile = Files.createTempFile("prefix", ".tmp");
Path tempDir = Files.createTempDirectory("my-temp");
System.out.println("临时文件:" + tempFile);
System.out.println("临时目录:" + tempDir);
3. 读取和写入文件 —— 最常用的操作
NIO.2 让文件读写变得异常简单。你不再需要手动处理 InputStream、Reader 和循环了。
3.1 读取所有字节/行(小文件)
对于小文件(比如配置文件、日志片段),你可以一次性读取所有内容:
import java.nio.file.Files;
import java.nio.file.Paths;
import java.io.IOException;
import java.util.List;
public class ReadWriteDemo {
public static void main(String[] args) throws IOException {
Path path = Paths.get("test.txt");
// 1. 读取所有字节(返回 byte[])
byte[] bytes = Files.readAllBytes(path);
String content = new String(bytes, "UTF-8");
System.out.println("内容:" + content);
// 2. 读取所有行(返回 List<String>)
List<String> lines = Files.readAllLines(path, java.nio.charset.StandardCharsets.UTF_8);
lines.forEach(System.out::println);
// 3. 写入字节
String newContent = "Hello, NIO!";
Files.write(path, newContent.getBytes("UTF-8"));
// 4. 写入行(覆盖)
List<String> newLines = List.of("第一行", "第二行", "第三行");
Files.write(path, newLines, java.nio.charset.StandardCharsets.UTF_8);
// 5. 追加到文件
Files.write(path, "追加的内容".getBytes("UTF-8"),
java.nio.file.StandardOpenOption.APPEND);
}
}
Files.write() 默认是覆盖模式。如果想追加,传入 StandardOpenOption.APPEND。
3.2 流式读取(大文件)
对于大文件,一次性读取所有行可能会占用大量内存。这时可以用 Files.lines() 方法,它返回一个 Stream<String>,可以逐行处理,不会一次性加载全部内容到内存。
import java.nio.file.Files;
import java.nio.file.Paths;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
public class StreamLinesDemo {
public static void main(String[] args) throws IOException {
Path path = Paths.get("large_log.txt");
// 使用 try-with-resources 确保 Stream 被关闭
try (var lines = Files.lines(path, StandardCharsets.UTF_8)) {
// 只处理前 10 行
lines.limit(10)
.filter(line -> line.contains("ERROR"))
.forEach(System.out::println);
}
}
}
Files.lines() 返回的 Stream 是延迟加载的,并且必须用 try-with-resources 关闭,否则可能造成资源泄漏。
3.3 读写二进制文件
同样用 readAllBytes() 和 write() 即可:
Path src = Paths.get("image.jpg");
Path dest = Paths.get("copy.jpg");
byte[] data = Files.readAllBytes(src);
Files.write(dest, data); // 二进制复制
4. 复制、移动、删除文件
4.1 复制文件
Path source = Paths.get("source.txt");
Path target = Paths.get("target.txt");
// 复制文件(如果目标已存在会抛出 FileAlreadyExistsException)
Files.copy(source, target);
// 覆盖已存在的目标
Files.copy(source, target, java.nio.file.StandardCopyOption.REPLACE_EXISTING);
// 复制到目录(保持文件名)
Path destDir = Paths.get("backup/");
Files.copy(source, destDir.resolve(source.getFileName()));
Files.copy() 也支持复制目录(但不会递归复制内容,需要自己遍历)。
4.2 移动文件(或重命名)
Path source = Paths.get("data.txt");
Path target = Paths.get("archive/data.txt");
// 移动文件(如果目标已存在会抛异常)
Files.move(source, target);
// 覆盖已有文件,并原子地执行移动(如果文件系统支持)
Files.move(source, target,
java.nio.file.StandardCopyOption.REPLACE_EXISTING,
java.nio.file.StandardCopyOption.ATOMIC_MOVE);
移动和复制都可以设置选项:REPLACE_EXISTING(覆盖)、ATOMIC_MOVE(原子移动)、COPY_ATTRIBUTES(复制文件属性)。
4.3 删除文件或目录
Path file = Paths.get("temp.txt");
// 删除文件(如果文件不存在抛出 NoSuchFileException)
Files.delete(file);
// 安全删除:如果文件不存在,不抛出异常,返回 boolean
boolean deleted = Files.deleteIfExists(file);
System.out.println("是否删除成功:" + deleted);
// 删除目录(必须为空)
Path emptyDir = Paths.get("mydir");
Files.deleteIfExists(emptyDir);
要删除非空目录,需要递归遍历删除所有子文件和子目录(后面会讲如何遍历)。
5. 目录遍历 —— 探索文件树
NIO.2 提供了强大的目录遍历能力,可以很方便地列出目录内容、搜索文件、递归处理整个目录树。
5.1 列出目录内容(浅层)
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.io.IOException;
import java.util.stream.Stream;
public class ListDirDemo {
public static void main(String[] args) throws IOException {
Path dir = Paths.get(".");
// 方式一:Files.list() 返回 Stream<Path>(只包含一层)
try (Stream<Path> entries = Files.list(dir)) {
entries.forEach(System.out::println);
}
// 方式二:只遍历目录(过滤)
try (Stream<Path> entries = Files.list(dir)) {
entries.filter(Files::isDirectory)
.forEach(System.out::println);
}
}
}
Files.list() 不是递归的,只列出当前目录的直接子项。
5.2 递归遍历整个目录树
如果需要递归遍历所有子目录,使用 Files.walk()。它返回一个 Stream<Path>,深度优先遍历整个目录树。
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.io.IOException;
public class WalkDemo {
public static void main(String[] args) throws IOException {
Path start = Paths.get("src");
// 遍历 src 目录下的所有文件(包括子目录)
try (var paths = Files.walk(start)) {
paths.filter(Files::isRegularFile)
.forEach(System.out::println);
}
// 限制遍历深度(比如只遍历两层)
try (var paths = Files.walk(start, 2)) {
paths.forEach(System.out::println);
}
}
}
5.3 搜索文件(Find)
Files.find() 方法可以结合深度和谓词(条件)来搜索文件。
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.io.IOException;
import java.nio.file.attribute.BasicFileAttributes;
public class FindDemo {
public static void main(String[] args) throws IOException {
Path start = Paths.get(".");
// 搜索所有扩展名为 .txt 或 .log 的文件
try (var paths = Files.find(start, Integer.MAX_VALUE,
(path, attrs) -> {
String name = path.getFileName().toString();
return name.endsWith(".txt") || name.endsWith(".log");
})) {
paths.forEach(System.out::println);
}
// 搜索最近 7 天内修改过的文件(需要 BasicFileAttributes)
long sevenDaysAgo = System.currentTimeMillis() – 7 * 24 * 3600 * 1000;
try (var paths = Files.find(start, Integer.MAX_VALUE,
(path, attrs) -> attrs.lastModifiedTime().toMillis() > sevenDaysAgo)) {
paths.forEach(System.out::println);
}
}
}
Files.find() 比 walk() + filter() 更高效,因为它可以在遍历过程中就进行筛选,且能访问文件属性。
5.4 用 FileVisitor 进行更精细的控制
如果需要更复杂的遍历逻辑(比如在遍历中删除文件、处理符号链接等),可以使用 Files.walkFileTree() + SimpleFileVisitor。这是传统方式,适合需要精细控制的场景。
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.io.IOException;
public class DeleteDirVisitor extends SimpleFileVisitor<Path> {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
Files.delete(file); // 删除文件
System.out.println("删除文件:" + file);
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
Files.delete(dir); // 删除目录(此时已空)
System.out.println("删除目录:" + dir);
return FileVisitResult.CONTINUE;
}
public static void main(String[] args) throws IOException {
Path toDelete = Paths.get("temp");
Files.walkFileTree(toDelete, new DeleteDirVisitor());
System.out.println("删除完成!");
}
}
这种方式的优势是你能精确控制访问每个文件/目录时的行为(继续、跳过、终止),还能处理异常。
6. 文件属性 —— 获取更多元数据
NIO.2 提供了统一的文件属性访问接口,支持不同操作系统的特定属性。
import java.nio.file.*;
import java.io.IOException;
import java.nio.file.attribute.BasicFileAttributes;
public class AttrDemo {
public static void main(String[] args) throws IOException {
Path path = Paths.get("test.txt");
// 获取基础属性(所有文件系统都支持)
BasicFileAttributes attrs = Files.readAttributes(path, BasicFileAttributes.class);
System.out.println("创建时间:" + attrs.creationTime());
System.out.println("最后修改时间:" + attrs.lastModifiedTime());
System.out.println("文件大小:" + attrs.size());
System.out.println("是否是目录:" + attrs.isDirectory());
// 获取 Posix 属性(Linux/macOS 等 Unix 系统)
if (FileSystems.getDefault().supportedFileAttributeViews().contains("posix")) {
java.nio.file.attribute.PosixFileAttributes posixAttrs =
Files.readAttributes(path, java.nio.file.attribute.PosixFileAttributes.class);
System.out.println("权限:" + java.nio.file.attribute.PosixFilePermissions.toString(posixAttrs.permissions()));
System.out.println("所有者:" + posixAttrs.owner());
}
// 设置文件权限(仅在 Posix 系统有效)
if (FileSystems.getDefault().supportedFileAttributeViews().contains("posix")) {
java.nio.file.attribute.PosixFilePermission ownerRead = java.nio.file.attribute.PosixFilePermission.OWNER_READ;
java.util.Set<java.nio.file.attribute.PosixFilePermission> perms =
java.nio.file.attribute.PosixFilePermissions.fromString("rw-r–r–");
Files.setPosixFilePermissions(path, perms);
}
}
}
在 Windows 下,你可以通过 DosFileAttributes 获取隐藏、只读等属性。
7. 与旧 I/O 的互操作
NIO.2 与传统的 File 和流可以互相转换:
- Path → File:path.toFile()
- File → Path:file.toPath()
- Path → InputStream/OutputStream:Files.newInputStream(path) 和 Files.newOutputStream(path)
- Path → Reader/Writer:Files.newBufferedReader(path) 和 Files.newBufferedWriter(path)
// 传统 File 转 Path
File oldFile = new File("test.txt");
Path path = oldFile.toPath();
// 使用 Files 工具类创建流
try (InputStream in = Files.newInputStream(path);
BufferedReader reader = Files.newBufferedReader(path, StandardCharsets.UTF_8)) {
// 混用 NIO 和传统流 API
}
你可以逐步迁移代码,不用一次性重写所有旧的 I/O 代码。
8. 综合示例 —— 一个完整的文件工具类
我们把今天学到的常用操作封装成一个工具类,方便以后复用:
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.util.List;
import java.util.stream.Stream;
public class FileUtil {
/**
* 读取文件所有行(小文件)
*/
public static List<String> readAllLines(String path) throws IOException {
return Files.readAllLines(Paths.get(path), StandardCharsets.UTF_8);
}
/**
* 写入文件(覆盖)
*/
public static void writeAllLines(String path, List<String> lines) throws IOException {
Files.write(Paths.get(path), lines, StandardCharsets.UTF_8);
}
/**
* 追加一行到文件末尾
*/
public static void appendLine(String path, String line) throws IOException {
Path p = Paths.get(path);
// 确保父目录存在
if (p.getParent() != null) {
Files.createDirectories(p.getParent());
}
String content = line + System.lineSeparator();
Files.write(p, content.getBytes(StandardCharsets.UTF_8),
StandardOpenOption.APPEND, StandardOpenOption.CREATE);
}
/**
* 复制文件,自动创建目标目录
*/
public static void copyFile(String source, String target) throws IOException {
Path src = Paths.get(source);
Path dest = Paths.get(target);
// 确保目标目录存在
if (dest.getParent() != null) {
Files.createDirectories(dest.getParent());
}
Files.copy(src, dest, StandardCopyOption.REPLACE_EXISTING);
}
/**
* 移动文件,自动创建目标目录
*/
public static void moveFile(String source, String target) throws IOException {
Path src = Paths.get(source);
Path dest = Paths.get(target);
if (dest.getParent() != null) {
Files.createDirectories(dest.getParent());
}
Files.move(src, dest, StandardCopyOption.REPLACE_EXISTING);
}
/**
* 删除目录及所有内容(递归)
*/
public static void deleteDirectory(String dirPath) throws IOException {
Path root = Paths.get(dirPath);
if (!Files.exists(root)) return;
try (Stream<Path> walk = Files.walk(root)) {
// 必须先删除子文件,再删除目录本身(逆序)
walk.sorted((a, b) -> b.toString().compareTo(a.toString()))
.forEach(p -> {
try {
Files.deleteIfExists(p);
} catch (IOException e) {
throw new RuntimeException("删除失败:" + p, e);
}
});
}
}
/**
* 搜索目录下所有匹配扩展名的文件
*/
public static List<Path> searchByExtension(String rootPath, String extension) throws IOException {
Path start = Paths.get(rootPath);
try (Stream<Path> paths = Files.find(start, Integer.MAX_VALUE,
(p, attrs) -> p.getFileName().toString().endsWith(extension))) {
return paths.toList();
}
}
/**
* 获取文件大小(人性化显示)
*/
public static String getHumanReadableSize(String path) throws IOException {
long size = Files.size(Paths.get(path));
String[] units = {"B", "KB", "MB", "GB", "TB"};
int idx = 0;
double s = size;
while (s >= 1024 && idx < units.length – 1) {
s /= 1024;
idx++;
}
return String.format("%.2f %s", s, units[idx]);
}
public static void main(String[] args) throws IOException {
// 测试各个方法
appendLine("logs/app.log", "程序启动于 " + java.time.LocalDateTime.now());
copyFile("config.properties", "backup/config_backup.properties");
System.out.println("文件大小:" + getHumanReadableSize("config.properties"));
List<Path> txtFiles = searchByExtension(".", ".txt");
System.out.println("找到 " + txtFiles.size() + " 个 txt 文件");
}
}
9. 传统 I/O vs NIO.2 —— 何时用哪个?
| 新项目、新代码 | NIO.2 + Files 工具类,更简洁、更现代 |
| 需要处理大文件、高性能 | NIO.2(支持内存映射、异步 I/O) |
| 需要与旧的 File 生态对接 | NIO.2 可互操作 |
| 需要手动控制缓冲、底层细节 | 传统流可能更灵活,但通常不必要 |
| 简单读写小文件 | Files.readAllLines() / Files.write() |
总结:从现在开始,优先使用 NIO.2 的 Path 和 Files,它们会让你的文件操作代码更短、更清晰、更不容易出错。
10. 今天的总结
今天我们全面学习了 NIO.2 的文件操作:
- Path:代表文件或目录路径,提供了丰富的路径操作方法。
- Files:包含了几乎所有文件操作的静态方法:exists、createFile、createDirectories、copy、move、delete、readAllBytes、readAllLines、write 等。
- Files.lines():流式读取大文件,逐行处理。
- 目录遍历:Files.list()(一层)、Files.walk()(递归)、Files.find()(带条件搜索)、walkFileTree()(精细控制)。
- 文件属性:通过 Files.readAttributes() 获取创建时间、修改时间、大小、权限等。
- 与传统 I/O 互操作:toFile() / toPath() / Files.newInputStream() 等。
NIO.2 让 Java 的文件操作从“繁琐”变成了“简洁”。现在你已经掌握了最现代、最推荐的文件处理方式。
动手试试
写一个程序,把当前目录下所有 .java 文件复制到 backup 目录下,保持原有的目录结构(提示:用 Files.walk() 和 Files.copy())。
写一个 FileSizeCounter 方法,遍历一个目录,计算所有文件的总大小,并输出每个子目录的大小(类似 Linux 的 du 命令)。
写一个程序,从用户输入一个目录路径,然后定期(每隔 10 秒)检查该目录下是否有新增的 .tmp 文件,如果有则打印文件名(提示:用 WatchService,这是 NIO.2 的文件监听机制,可以选做)。
把你之前的学生管理系统中的数据存储功能改为用 Files.write() 和 Files.readAllLines() 保存和加载数据(比如保存到 students.txt 中),体会用 NIO.2 做持久化是多么简单。
I/O 流和 NIO 的内容到这里就结束了。下一篇文章,我们将学习 日期与时间 API(java.time)——这是 Java 8 引入的一套全新的、设计优秀的日期时间库,它将彻底取代旧的 Date 和 Calendar。
我们下一篇见。😃
📌 获取本系列示例代码请访问 GitCode。
网硕互联帮助中心


评论前必须登录!
注册