乐途乐途
主页
  • 计算机基础

    • TCP/IP
    • Linux
    • HTTP
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C
    • C++
    • Java SE
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • Maven 入门
    • Maven 进阶
    • MyBatis
    • Spring
    • Spring MVC
  • Java

    • Spring Boot
    • Spring Cloud
    • Spring Cloud Alibaba
    • Spring Security
    • Spring AI
    • Spring Batch
    • Kafka
    • Java 设计模式
  • 缓存

    • Redis
  • 搜索引擎

    • Elasticsearch
  • 分布式协调

    • ZooKeeper
联系
阿里云
主页
  • 计算机基础

    • TCP/IP
    • Linux
    • HTTP
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C
    • C++
    • Java SE
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • Maven 入门
    • Maven 进阶
    • MyBatis
    • Spring
    • Spring MVC
  • Java

    • Spring Boot
    • Spring Cloud
    • Spring Cloud Alibaba
    • Spring Security
    • Spring AI
    • Spring Batch
    • Kafka
    • Java 设计模式
  • 缓存

    • Redis
  • 搜索引擎

    • Elasticsearch
  • 分布式协调

    • ZooKeeper
联系
阿里云
  • 学习路径
  • 第1章 Java概述与环境搭建

    • Java概述与环境搭建
    • Java语言概述
    • 解释型语言与编译型语言对比
    • JDK安装与配置
    • JDK、JRE、JVM 详解
    • HelloWorld程序详解
    • IDE 介绍
  • 第2章 标识符与基本数据类型

    • 章节导读
    • 变量概述
    • 常量概述
    • 基本类型与包装类
    • 字节型 byte
    • 短整型 short
    • 整型 int
    • 长整型 long
    • 单精度浮点型 float
    • 双精度浮点型 double
    • 字符型 char
    • 布尔型 boolean
    • 类型转换
  • 第3章 运算符与表达式

    • 章节导读
    • 算术运算符
    • 赋值运算符
    • 关系运算符
    • 逻辑运算符
    • 位运算符
    • 条件运算符
    • 运算符优先级
    • 表达式
  • 第4章 流程控制

    • 章节导读
    • 常见的程序运行流程
    • if-else 选择结构
    • switch 多分支选择
    • while 循环
    • do-while 循环
    • for 循环
    • break 与 continue
  • 第5章 数组

    • 章节导读
    • 一维数组
    • 多维数组
    • Arrays 工具类
  • 第6章 类与对象

    • 章节导读
    • 类与对象
    • 方法定义与调用
    • 构造方法
    • 封装
    • 访问修饰符
    • package 与 import
    • static 关键字
    • this 关键字
    • 参数传递 详解
    • 枚举
    • 成员内部类
    • 局部内部类
    • 静态内部类
    • 匿名内部类
  • 第7章 接口与继承

    • 章节导读
    • 继承
    • super 关键字
    • final 关键字
    • 多态
    • 向上转型与向下转型
    • 抽象类
    • 接口
    • 抽象类与接口对比
  • 第8章 注解

    • 章节导读
    • 注解基础
    • 元注解详解
    • 自定义注解
  • 第9章 常用类

    • 章节导读:Java 常用类
    • Object 类:万类之祖
    • 包装类:基本类型的对象化
    • String:不可变的字符串
    • StringBuffer:线程安全的可变字符串
    • StringBuilder:可变的字符串构建器
    • Math:数学运算工具类
    • Random:伪随机数生成器
    • 大数值运算 详解
    • 日期时间API 详解
  • 第10章 异常机制

    • 章节导读
    • 异常体系与分类
    • try-catch-finally
    • try-with-resources
    • throws 与 throw
    • 自定义异常
  • 第11章 泛型

    • 章节导读
    • 泛型基础
    • 通配符与PECS原则
    • 类型擦除
  • 第12章 集合框架

    • 章节导读
    • 集合框架概述
    • ArrayList
    • LinkedList
    • HashMap 详解
    • LinkedHashMap 详解
    • TreeMap 详解
    • HashSet
    • TreeSet 详解
    • TreeSet 与 Comparable
    • Collections 工具类详解
  • 第13章 IO流

    • 章节导读
    • IO流概述
    • 字节流
    • 字符流
    • 缓冲流
    • 转换流 详解
    • 序列化 详解
    • NIO与Files 详解
    • NIO与Files工具类
  • 第14章 多线程与并发

    • 第十六章 多线程与并发 —— 章节导读
    • 线程基础详解
    • synchronized 详解
    • Lock 与显式锁详解
    • volatile 详解
    • wait 与 notify 详解
    • ThreadLocal详解
    • 原子类详解
    • 并发工具类详解
    • 线程池详解
  • 第15章 反射

    • 章节导读
    • 反射概述与 Class 对象
    • Constructor 与对象创建
    • Field与Method详解
    • 反射应用详解
  • 第16章 JDK8新特性

    • 章节导读
    • Lambda 表达式
    • Stream API 基础
    • Stream API 高级详解
    • Optional 详解
    • 新日期时间API详解
  • 第17章 JDK9-11新特性

    • 章节导读
    • 模块化系统 — Project Jigsaw(JDK 9)
    • var 局部变量类型推断(JDK 10)
    • 集合工厂方法与增强(JDK 9 / 10 / 11)
    • 接口增强:private 方法(JDK 9)
    • Stream API 增强(JDK 9)
    • Optional 增强(JDK 9 / 10 / 11)
    • String 新增方法(JDK 11)
    • HTTP Client 与 Files 增强(JDK 11)
    • 直接运行 Java 源文件 — JEP 330(JDK 11)
  • 第18章 JDK12-17新特性

    • 章节导读
    • Switch 表达式(JDK 12 预览 / JDK 14 正式)
    • 文本块 Text Blocks(JDK 13 预览 / JDK 15 正式)
    • Records 记录类(JDK 14 预览 / JDK 16 正式)
    • 密封类 Sealed Classes(JDK 15 预览 / JDK 17 正式)
    • instanceof 模式匹配(JDK 14 预览 / JDK 16 正式)
    • Switch 模式匹配 — Pattern Matching for switch(JDK 17 预览 / JDK 21 正式)
    • Helpful NPE 与 String 增强(JDK 12 / JDK 14 / JDK 15)
    • Stream 增强(JDK 12 / JDK 16)
    • 日期时间增强 — Day Period 支持(JDK 16)
  • 第19章 JDK18-21新特性

    • 章节导读
    • 虚拟线程(JDK 19 预览 / JDK 20 第二预览 / JDK 21 正式)
    • 序列集合(JDK 21 正式)
    • Switch 模式匹配(JDK 17 预览 / JDK 18 第二预览 / JDK 20 第四预览 / JDK 21 正式)
    • Record 模式匹配(JDK 19 预览 / JDK 20 第二预览 / JDK 21 正式)
    • 未命名模式与变量(JDK 21 预览 / JDK 22 正式)
  • 第20章 JDK 22-25 新特性

    • 章节导读
    • 字符串模板(JDK 22 预览 / JDK 23 第二预览 / JDK 24 第三预览)
    • Stream Gatherers(JDK 22 预览 / JDK 24 第二预览)
    • 隐式声明类与实例方法(JDK 23 预览 / JDK 24 第二预览)
    • 原始类型模式匹配(JDK 24 预览)
  • 附录

    • Java 核心知识点
    • Java SE 专业术语
    • Java特性索引(JDK 8 → 25)

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 与外部世界对话的语言。

上一页
NIO与Files 详解