NIO与Files工具类
大翔在代码评审时皱起眉头:"小崔,你怎么还在用
new File("path")和File.listFiles()?JDK 7 的 NIO.2 已经出来好几年了,Files和Path更安全、更简洁,还能监控文件变化。"
小崔打开JDK文档,发现Files类竟然提供了200多个静态方法——文件操作从此变得像搭积木一样简单。
一、NIO.2 概述
NIO.2(New I/O 2)是 JDK 7 引入的文件系统API(JSR 203),在 JDK 8 中完全可用。它包含三个核心部分:
File vs Path 对比
| 特性 | java.io.File (JDK 1.0) | java.nio.file.Path (JDK 7) |
|---|---|---|
| 不可变性 | 可变 | 不可变(线程安全) |
| 异常信息 | false/null 表示失败(信息少) | 抛出明确异常(信息丰富) |
| 符号链接 | 不支持 | 完整支持 |
| 路径操作 | 字符串拼接(易出错) | resolve() / relativize() 等方法 |
| 文件属性 | 有限 | 完整支持 POSIX / Windows 属性 |
| 遍历目录 | listFiles()(一次性加载) | walk()(惰性流式,懒加载) |
| 监控 | 不支持 | WatchService 原生支持 |
二、Path 接口
2.1 创建 Path
// Paths.get() 创建 Path(JDK 7/8 唯一方式)
Path p1 = Paths.get("C:/feixiang/data/employees.csv");
Path p2 = Paths.get("C:", "feixiang", "data", "employees.csv"); // 可变参数
Path p3 = Paths.get("C:/feixiang", "data/employees.csv"); // 自动拼接
// File → Path(桥接旧API)
File oldFile = new File("C:/data");
Path path = oldFile.toPath();
// Path → File(桥接回旧API)
File backToFile = path.toFile();
JDK 11 新增
Path.of()替代Paths.get(),但 JDK 8 只能用Paths.get()。
2.2 Path 核心方法
| 方法 | 返回类型 | 说明 |
|---|---|---|
getFileName() | Path | 文件名(最后一级) |
getParent() | Path | 父目录路径 |
getRoot() | Path | 根路径 |
getName(int index) | Path | 第index级路径组件 |
getNameCount() | int | 路径层级数 |
resolve(String other) | Path | 拼接路径 |
relativize(Path other) | Path | 计算相对路径 |
normalize() | Path | 规范化(去除 . 和 ..) |
toAbsolutePath() | Path | 转为绝对路径 |
toFile() | File | 转为旧版File对象 |
startsWith(Path) | boolean | 是否以某路径开头 |
endsWith(Path) | boolean | 是否以某路径结尾 |
三、Files 工具类
Files 是 NIO.2 的瑞士军刀,全部为静态方法。下表按使用频率分类:
3.1 读写操作
| 方法 | 返回类型 | 说明 |
|---|---|---|
readAllLines(Path, Charset) | List<String> | 一次读取所有行(小文件) |
readAllBytes(Path) | byte[] | 一次读取所有字节 |
lines(Path, Charset) | Stream<String> | 惰性流式读取(JDK 8 新增,大文件友好) |
write(Path, byte[], OpenOption...) | Path | 写入字节数组 |
write(Path, Iterable<CharSequence>, Charset, OpenOption...) | Path | 写入多行文本 |
newBufferedReader(Path, Charset) | BufferedReader | 创建带缓冲的字符读取器 |
newBufferedWriter(Path, Charset, OpenOption...) | BufferedWriter | 创建带缓冲的字符写出器 |
newInputStream(Path, OpenOption...) | InputStream | 创建字节输入流 |
newOutputStream(Path, OpenOption...) | OutputStream | 创建字节输出流 |
3.2 文件/目录操作
| 方法 | 返回类型 | 说明 |
|---|---|---|
copy(Path, Path, CopyOption...) | Path | 复制文件或目录 |
move(Path, Path, CopyOption...) | Path | 移动/重命名文件或目录 |
delete(Path) | void | 删除文件(不存在则抛异常) |
deleteIfExists(Path) | boolean | 删除文件(不存在返回false) |
createDirectory(Path, FileAttribute...) | Path | 创建单级目录 |
createDirectories(Path, FileAttribute...) | Path | 创建多级目录(类似 mkdir -p) |
createFile(Path, FileAttribute...) | Path | 创建空文件 |
createTempFile(String, String, FileAttribute...) | Path | 创建临时文件 |
createTempDirectory(String, FileAttribute...) | Path | 创建临时目录 |
3.3 查询与判断
| 方法 | 返回类型 | 说明 |
|---|---|---|
exists(Path, LinkOption...) | boolean | 文件/目录是否存在 |
isDirectory(Path, LinkOption...) | boolean | 是否为目录 |
isRegularFile(Path, LinkOption...) | boolean | 是否为常规文件 |
isReadable(Path) | boolean | 是否可读 |
isWritable(Path) | boolean | 是否可写 |
isExecutable(Path) | boolean | 是否可执行 |
size(Path) | long | 文件大小(字节) |
getLastModifiedTime(Path, LinkOption...) | FileTime | 最后修改时间 |
getOwner(Path, LinkOption...) | UserPrincipal | 文件所有者 |
3.4 遍历与搜索
| 方法 | 返回类型 | 说明 |
|---|---|---|
walk(Path, int, FileVisitOption...) | Stream<Path> | 惰性遍历目录树(JDK 8 Stream) |
walk(Path, FileVisitOption...) | Stream<Path> | 遍历所有层级(无深度限制) |
list(Path) | Stream<Path> | 列出当前目录内容(单层) |
find(Path, int, BiPredicate, FileVisitOption...) | Stream<Path> | 按条件搜索文件 |
walkFileTree(Path, FileVisitor) | Path | 使用回调遍历(传统方式) |
四、DirectoryStream 与 FileVisitor
4.1 DirectoryStream(单层目录遍历)
// 传统方式:遍历目录
try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir)) {
for (Path entry : stream) {
System.out.println(entry.getFileName());
}
}
// 带过滤的遍历
try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir, "*.{java,class}")) {
// 只匹配 .java 和 .class 文件
}
DirectoryStream适合单层遍历,用walk()或walkFileTree()遍历整个目录树。
4.2 FileVisitor(目录树遍历回调)
// SimpleFileVisitor 提供了默认空实现,只需覆盖需要的方法
Files.walkFileTree(startDir, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
// 处理每个文件
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) {
// 进入目录前
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFileFailed(Path file, IOException exc) {
// 文件访问失败时
return FileVisitResult.CONTINUE;
}
});
| FileVisitResult 值 | 含义 |
|---|---|
CONTINUE | 继续遍历 |
SKIP_SUBTREE | 跳过当前目录的子目录 |
SKIP_SIBLINGS | 跳过同级目录/文件 |
TERMINATE | 终止遍历 |
五、WatchService 文件系统监听
WatchService 可以监控目录中的文件变化,适合实现热部署配置、上传目录监控等功能。
局限性
| 限制 | 说明 |
|---|---|
| 不可靠 | 事件可能丢失(OS缓冲区溢出) |
| 平台依赖 | 某些OS不支持文件修改监听 |
| 不能递归 | 仅监听注册的目录,子目录需单独注册 |
| 效率低 | 大量文件操作时可能漏事件 |
生产环境中,WatchService 更适合轻量级监控。大规模文件同步建议用专业工具(如 Apache Commons IO 的
FileAlterationMonitor)。
六、完整示例
示例一:飞翔科技配置文件管理(NIO.2版)
场景:小崔用 NIO.2 重写配置文件读写,支持自动备份和流式搜索。
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* 飞翔科技 - 配置管理器(NIO.2版)
* 使用 Files 工具类实现配置文件的读写、备份和搜索
*/
public class ConfigManagerNIO {
private static final Path CONFIG_DIR = Paths.get("C:/feixiang/config");
private static final Path CONFIG_FILE = CONFIG_DIR.resolve("server.conf");
private static final Path BACKUP_DIR = CONFIG_DIR.resolve("backup");
public static void main(String[] args) {
System.out.println("=== 飞翔科技配置管理器(NIO.2版)===\n");
// 1. 确保目录存在
initDirectories();
// 2. 写入配置
writeConfig();
// 3. 读取配置
readConfig();
// 4. 搜索配置项
searchConfig("host");
// 5. 备份配置
backupConfig();
// 6. 列出所有备份
listBackups();
// 7. 流式搜索所有配置文件中的关键字
searchAllConfigs("admin");
}
/** 初始化目录结构 */
private static void initDirectories() {
try {
if (!Files.exists(CONFIG_DIR)) {
Files.createDirectories(CONFIG_DIR);
System.out.println("[初始化] 创建配置目录:" + CONFIG_DIR);
}
if (!Files.exists(BACKUP_DIR)) {
Files.createDirectories(BACKUP_DIR);
System.out.println("[初始化] 创建备份目录:" + BACKUP_DIR);
}
} catch (IOException e) {
throw new RuntimeException("目录初始化失败", e);
}
}
/** 写入配置文件 */
private static void writeConfig() {
try {
List<String> lines = List.of(
"# 飞翔科技服务器配置",
"# 生成时间:" + java.time.LocalDateTime.now(),
"db.host=192.168.1.100",
"db.port=3306",
"db.name=feixiang",
"admin.name=大翔",
"admin.email=daxiang@feixiang.com",
"log.level=INFO",
"log.path=/var/log/feixiang/"
);
// 一行代码写入所有行(指定UTF-8编码)
Files.write(CONFIG_FILE, lines, StandardCharsets.UTF_8);
long size = Files.size(CONFIG_FILE);
System.out.println("[写入] 配置文件已保存:" + CONFIG_FILE);
System.out.println("[写入] 文件大小:" + size + " bytes");
} catch (IOException e) {
System.err.println("配置写入失败:" + e.getMessage());
}
}
/** 读取配置文件 */
private static void readConfig() {
try {
System.out.println("\n[读取] 配置文件内容:");
System.out.println("--------------------------------");
// JDK 8 新增:lines() 返回 Stream,惰性读取(大文件也安全)
try (Stream<String> lines = Files.lines(CONFIG_FILE, StandardCharsets.UTF_8)) {
lines.forEach(line -> System.out.println(" " + line));
}
} catch (IOException e) {
System.err.println("配置读取失败:" + e.getMessage());
}
}
/** 搜索包含关键字的配置行 */
private static void searchConfig(String keyword) {
try {
System.out.println("\n[搜索] 包含 \"" + keyword + "\" 的配置项:");
try (Stream<String> lines = Files.lines(CONFIG_FILE, StandardCharsets.UTF_8)) {
List<String> results = lines
.filter(line -> line.contains(keyword))
.collect(Collectors.toList());
if (results.isEmpty()) {
System.out.println(" (无匹配项)");
} else {
results.forEach(line -> System.out.println(" " + line));
}
}
} catch (IOException e) {
System.err.println("搜索失败:" + e.getMessage());
}
}
/** 备份配置文件 */
private static void backupConfig() {
try {
String timestamp = java.time.LocalDateTime.now()
.format(java.time.format.DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss"));
Path backupFile = BACKUP_DIR.resolve("server_" + timestamp + ".conf");
Files.copy(CONFIG_FILE, backupFile, StandardCopyOption.REPLACE_EXISTING);
System.out.println("\n[备份] 已备份到:" + backupFile.getFileName());
} catch (IOException e) {
System.err.println("备份失败:" + e.getMessage());
}
}
/** 列出所有备份文件 */
private static void listBackups() {
try {
System.out.println("\n[备份列表] " + BACKUP_DIR);
try (Stream<Path> files = Files.list(BACKUP_DIR)) {
files
.sorted((a, b) -> {
try {
return Files.getLastModifiedTime(b).compareTo(
Files.getLastModifiedTime(a));
} catch (IOException e) {
return 0;
}
})
.forEach(p -> {
try {
System.out.printf(" %-35s %d bytes%n",
p.getFileName(), Files.size(p));
} catch (IOException ignored) {}
});
}
} catch (IOException e) {
System.err.println("列出备份失败:" + e.getMessage());
}
}
/** 流式搜索所有配置文件 */
private static void searchAllConfigs(String keyword) {
try {
System.out.println("\n[全局搜索] 在所有配置文件中搜索 \"" + keyword + "\":");
Files.walk(CONFIG_DIR, 1) // 只遍历配置目录自身及其直接子文件
.filter(Files::isRegularFile)
.filter(p -> p.toString().endsWith(".conf"))
.forEach(p -> {
try (Stream<String> lines = Files.lines(p, StandardCharsets.UTF_8)) {
lines
.filter(line -> line.contains(keyword))
.forEach(line ->
System.out.printf(" %-30s | %s%n",
p.getFileName(), line));
} catch (IOException ignored) {}
});
} catch (IOException e) {
System.err.println("全局搜索失败:" + e.getMessage());
}
}
}
运行输出:
=== 飞翔科技配置管理器(NIO.2版)===
[初始化] 创建配置目录:C:\feixiang\config
[初始化] 创建备份目录:C:\feixiang\config\backup
[写入] 配置文件已保存:C:\feixiang\config\server.conf
[写入] 文件大小:241 bytes
[读取] 配置文件内容:
--------------------------------
# 飞翔科技服务器配置
# 生成时间:2025-01-15T15:00:00
db.host=192.168.1.100
db.port=3306
db.name=feixiang
admin.name=大翔
admin.email=daxiang@feixiang.com
log.level=INFO
log.path=/var/log/feixiang/
[搜索] 包含 "host" 的配置项:
db.host=192.168.1.100
[备份] 已备份到:server_20250115_150000.conf
[备份列表] C:\feixiang\config\backup
server_20250115_150000.conf 241 bytes
[全局搜索] 在所有配置文件中搜索 "admin":
server.conf | admin.name=大翔
server.conf | admin.email=daxiang@feixiang.com
示例二:飞翔科技日志文件监控(WatchService)
场景:飞翔科技的日志处理系统需要实时监控日志目录,当有新的日志文件产生时自动通知处理。
import java.io.IOException;
import java.nio.file.*;
import java.util.concurrent.TimeUnit;
/**
* 飞翔科技 - 日志目录监控工具
* 使用 WatchService 监控文件创建、修改、删除事件
*/
public class LogDirectoryWatcher {
private final Path watchDir;
private final WatchService watchService;
private volatile boolean running = true;
public LogDirectoryWatcher(String dirPath) throws IOException {
this.watchDir = Paths.get(dirPath);
// 确保目录存在
if (!Files.exists(watchDir)) {
Files.createDirectories(watchDir);
}
// 创建 WatchService 并注册目录
this.watchService = FileSystems.getDefault().newWatchService();
watchDir.register(watchService,
StandardWatchEventKinds.ENTRY_CREATE, // 文件创建
StandardWatchEventKinds.ENTRY_MODIFY, // 文件修改
StandardWatchEventKinds.ENTRY_DELETE // 文件删除
);
System.out.println("[监控] 开始监控目录:" + watchDir.toAbsolutePath());
}
/** 启动监控(阻塞式) */
public void start() {
System.out.println("[监控] 按 Ctrl+C 停止监控...\n");
while (running) {
WatchKey key;
try {
// take() 阻塞等待事件(如果改用 poll(5, TimeUnit.SECONDS) 可超时返回)
key = watchService.take();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
} catch (ClosedWatchServiceException e) {
break;
}
for (WatchEvent<?> event : key.pollEvents()) {
WatchEvent.Kind<?> kind = event.kind();
// OVERFLOW:事件丢失(缓冲区溢出,需全量重新扫描)
if (kind == StandardWatchEventKinds.OVERFLOW) {
System.out.println("[警告] 事件丢失!可能有文件变化未被检测到");
continue;
}
// 获取变化的文件名
@SuppressWarnings("unchecked")
WatchEvent<Path> ev = (WatchEvent<Path>) event;
Path filename = ev.context();
// 事件类型中文描述
String eventType = getEventDescription(kind);
System.out.printf("[%s] %s - %s%n",
java.time.LocalTime.now().toString().substring(0, 8),
eventType, filename);
// 实际项目中在此处触发日志处理逻辑
}
// ⚡ 关键:reset() 重新注册,返回 false 表示目录不可再访问
if (!key.reset()) {
System.err.println("[错误] 目录不再可访问,停止监控:" + watchDir);
break;
}
}
}
/** 停止监控 */
public void stop() {
running = false;
try {
watchService.close();
} catch (IOException e) {
System.err.println("关闭 WatchService 失败");
}
System.out.println("\n[监控] 已停止");
}
private String getEventDescription(WatchEvent.Kind<?> kind) {
if (kind == StandardWatchEventKinds.ENTRY_CREATE) return "创建";
if (kind == StandardWatchEventKinds.ENTRY_MODIFY) return "修改";
if (kind == StandardWatchEventKinds.ENTRY_DELETE) return "删除";
return "未知";
}
/**
* 演示主方法
* 运行本程序后,可以手动在该目录下创建/修改/删除文件来观察输出
*/
public static void main(String[] args) throws Exception {
LogDirectoryWatcher watcher = new LogDirectoryWatcher("C:/feixiang/logs/watch");
// 启动一个线程模拟文件操作
Thread simulator = new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(2);
// 模拟创建日志文件
Path newLog = Paths.get("C:/feixiang/logs/watch/access_20250115.log");
Files.write(newLog, "192.168.1.100 - admin [15/Jan/2025:15:30:00]".getBytes());
System.out.println(" [模拟] 创建了日志文件");
TimeUnit.SECONDS.sleep(1);
// 模拟修改日志文件
Files.write(newLog, "新日志行".getBytes(), StandardOpenOption.APPEND);
System.out.println(" [模拟] 修改了日志文件");
TimeUnit.SECONDS.sleep(1);
// 模拟删除日志文件
Files.delete(newLog);
System.out.println(" [模拟] 删除了日志文件");
TimeUnit.SECONDS.sleep(2);
watcher.stop();
} catch (Exception e) {
e.printStackTrace();
}
});
simulator.setDaemon(true);
simulator.start();
watcher.start();
}
}
运行输出:
[监控] 开始监控目录:C:\feixiang\logs\watch
[监控] 按 Ctrl+C 停止监控...
[模拟] 创建了日志文件
[15:30:02] 创建 - access_20250115.log
[模拟] 修改了日志文件
[15:30:03] 修改 - access_20250115.log
[模拟] 删除了日志文件
[15:30:04] 删除 - access_20250115.log
[监控] 已停止
七、JDK 8 中 Files.lines() 的流式处理优势
// 传统方式:读取全部 → 内存爆炸
List<String> allLines = Files.readAllLines(hugeFile, UTF_8); // 可能OOM
// JDK 8 流式:惰性读取,只处理前5条ERROR行
try (Stream<String> lines = Files.lines(hugeFile, UTF_8)) {
lines.filter(line -> line.contains("ERROR"))
.limit(5)
.forEach(System.out::println);
}
// 内存中同时只保留少数几行
八、易错场景
反例一:Files.readAllLines() 用于大文件
// ❌ 错误:1GB日志文件一次性加载到内存 → OutOfMemoryError
List<String> lines = Files.readAllLines(
Paths.get("huge_server.log"), StandardCharsets.UTF_8);
纠正:
// ✅ 正确:流式处理,惰性加载
try (Stream<String> lines = Files.lines(Paths.get("huge_server.log"), UTF_8)) {
lines.filter(l -> l.contains("ERROR"))
.forEach(System.out::println);
}
反例二:Files.delete() 删除不存在的文件
// ❌ 错误:文件不存在时抛出 NoSuchFileException
Files.delete(Paths.get("temp.txt"));
// 异常:java.nio.file.NoSuchFileException: temp.txt
纠正:
// ✅ 正确:使用 deleteIfExists,不存在时静默返回 false
boolean deleted = Files.deleteIfExists(Paths.get("temp.txt"));
// deleted == false 表示文件本来就不存在,不是错误
反例三:忘记 flush 或 close Files.newBufferedWriter
// ❌ 错误:BufferedWriter 未关闭,缓冲区数据丢失
BufferedWriter writer = Files.newBufferedWriter(Paths.get("data.txt"), UTF_8);
writer.write("重要数据");
// 没有 close() —— 数据可能不完整!
纠正:
// ✅ 正确:try-with-resources
try (BufferedWriter writer = Files.newBufferedWriter(Paths.get("data.txt"), UTF_8)) {
writer.write("重要数据");
}
反例四:WatchService 忘记 reset()
// ❌ 错误:处理完事件未 reset,后续事件收不到
WatchKey key = watchService.take();
for (WatchEvent<?> event : key.pollEvents()) {
System.out.println("文件变化:" + event.context());
}
// 没有 key.reset() → 该key失效,目录不再被监听!
纠正:
// ✅ 正确:必须在处理完事件后调用 reset()
WatchKey key = watchService.take();
for (WatchEvent<?> event : key.pollEvents()) {
System.out.println("文件变化:" + event.context());
}
boolean valid = key.reset(); // 重新注册到 WatchService
if (!valid) {
System.err.println("目录不可访问,退出监控");
}
九、面试考点
Q1:java.io.File 和 java.nio.file.Path 的区别?
①
File是类,可变;Path是接口,不可变(线程安全)。②File异常信息贫瘠(返回 false/null),Path配合Files抛明确异常。③Path完整支持符号链接,File不直接支持。④Path有丰富的路径操作方法(resolve、relativize、normalize),File用字符串拼接。⑤Path与Files.walk()配合可以惰性流式遍历目录树。JDK 7+ 的代码应优先使用Path。
Q2:Files.lines() 和 Files.readAllLines() 的选择原则?
readAllLines()返回List<String>,将文件全部内容一次性加载到内存,适合小文件(<10MB)且需要随机访问的场景。lines()返回Stream<String>,采用惰性加载,适合大文件或只需要处理前N行/满足条件的行的场景。lines()返回的 Stream 必须在 try-with-resources 中使用,因为底层需要关闭文件句柄。
Q3:Files.walk() 和 Files.walkFileTree() 的区别?
walk()返回Stream<Path>,支持 Stream API 的 filter/map/collect 操作,是声明式、惰性的遍历方式(JDK 8)。walkFileTree()使用FileVisitor回调接口,是命令式、主动的遍历方式(JDK 7),适合需要在进入/退出目录时执行特定逻辑的场景(如统计目录大小、删除嵌套目录)。两者都需要用 try-with-resources 包裹,walk()的 Stream 需要关闭。
Q4:WatchService 的局限性和适用场景?
局限性:① 事件可能因OS缓冲区溢出而丢失(
OVERFLOW事件);② 不保证 100% 可靠,不适合金融/安全等要求精确的场景;③ 不能递归监控子目录(需手动递归注册);④ 部分平台不支持某些事件类型(如文件修改)。适用场景:配置热加载、上传目录监控(非关键数据)、开发环境文件变化检测。
Q5:JDK 8 中 Paths.get() 和 Path.of() 的区别?
Paths.get()是 JDK 7 引入的工厂方法,JDK 8 中唯一官方方式。Path.of()是 JDK 11 新增的接口静态方法,语义一致。在 JDK 8 中只能用Paths.get()。
大翔:"NIO.2 让文件操作现代化的同时,也提醒我们——选择正确的工具和方法很重要。小文件用 readAllLines,大文件用 lines(),监控用 WatchService,遍历用 walk()。工具选对了,事半功倍。"
章节结语:从字节流的底层传输到字符流的编码转换,从缓冲流的性能优化到转换流的桥梁作用,从序列化的对象持久化到 NIO.2 的现代化文件操作——Java IO 体系是层层递进、职责分明的。掌握它,你就掌握了 Java 与外部世界对话的语言。