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

    • 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 详解

场景:小崔被派去整理飞翔科技积累了三个月的服务器日志目录——里面有上千个日志文件,按日期命名但格式不统一,有的在子目录里,有的文件名带空格。小崔准备写个递归遍历脚本,大翔拦住了他:"用Java 7的NIO.2吧,Files.walkFileTree 几行就搞定了,比你自己写递归强多了。"白歌补充道:"还有 Files.readAllLines 读小文件、Files.copy 拷贝文件,JDK 7把以前那些繁琐的IO操作全简化了。"Frank人在海外,远程连过来说:"And don't use java.io.File anymore — java.nio.file.Path is the modern way. Seriously, it's 2014." 朱璐好奇:"那原来的File类是不是可以扔了?"白歌摇头:"也不能完全扔,很多老代码和第三方库还在用,你得知道怎么转换。"


一、定义表

概念定义
NIO.2JDK 7 引入的新IO API(JSR 203),包含 java.nio.file 包,提供对文件系统的增强操作
PathNIO.2 中代表文件系统路径的不可变对象,替代 java.io.File
Paths工具类,提供静态方法 get() 来创建 Path 实例
Files工具类,提供静态方法用于文件的创建、复制、移动、删除、读取属性等操作
FileSystem表示文件系统的抽象,可通过 FileSystems.getDefault() 获取默认文件系统
FileVisitor用于递归遍历文件树的访问者接口,包含 preVisitDirectory、visitFile、visitFileFailed、postVisitDirectory
SimpleFileVisitorFileVisitor 的简易实现,所有方法默认行为是继续遍历,可按需覆盖
FileVisitResult枚举,控制遍历行为:CONTINUE、TERMINATE、SKIP_SUBTREE、SKIP_SIBLINGS
WatchService文件系统监控服务,可监测目录中的文件创建、修改、删除事件
StandardOpenOption枚举,文件操作选项:CREATE、CREATE_NEW、APPEND、TRUNCATE_EXISTING、READ、WRITE等

二、Mermaid 类关系图


三、Path vs File 对比

特性java.io.File (Legacy)java.nio.file.Path (JDK 7+)
不可变性可变(如 renameTo 改变状态)不可变(所有操作返回新Path)
错误处理返回 boolean(失败只返回false)抛出具体异常(如 NoSuchFileException)
路径操作字符串拼接,容易出错resolve、relativize、normalize 等方法
符号链接不支持(始终跟随链接)可选择是否跟随符号链接
文件属性有限的属性支持丰富的 BasicFileAttributes、DosFileAttributes、PosixFileAttributes
遍历目录listFiles() 返回数组(非lazy)Files.walk() 返回 Stream(lazy)、Files.list() 返回 Stream
互操作file.toPath() 转换为Pathpath.toFile() 转换为File

四、Path API 核心操作详解

4.1 创建 Path

// 方式1: Paths.get()(推荐)
Path p1 = Paths.get("d:/feixiang/logs");
Path p2 = Paths.get("d:", "feixiang", "logs", "app.log");  // 可变参数
Path p3 = Paths.get("d:/feixiang", "data", "config.properties");

// 方式2: FileSystems.getDefault().getPath()
Path p4 = FileSystems.getDefault().getPath("d:/feixiang/logs");

// 方式3: 从 File 转换
File oldFile = new File("d:/feixiang/old.txt");
Path p5 = oldFile.toPath();  // File → Path
File backToFile = p5.toFile();  // Path → File

// 方式4: 相对路径
Path cwd = Paths.get("").toAbsolutePath();  // 当前工作目录

4.2 路径操作

Path path = Paths.get("d:/feixiang/logs/2024/app.log");

path.getFileName();       // app.log
path.getParent();         // d:\feixiang\logs\2024
path.getRoot();           // d:\
path.getName(0);          // feixiang
path.getName(1);          // logs
path.getNameCount();      // 5 (feixiang, logs, 2024, app.log —— 不含root)

// 路径拼接
Path base = Paths.get("d:/feixiang");
Path resolved = base.resolve("logs");      // d:\feixiang\logs
Path sibling = base.resolveSibling("data"); // d:\data (替换最后一段)

// 相对化
Path abs = Paths.get("d:/feixiang/logs/app.log");
Path base2 = Paths.get("d:/feixiang");
Path relative = base2.relativize(abs);      // logs\app.log

// 规范化
Path messy = Paths.get("d:/feixiang/../feixiang/logs/./app.log");
Path clean = messy.normalize();             // d:\feixiang\logs\app.log

// 比较
path.startsWith(Paths.get("d:/feixiang")); // true
path.endsWith(Paths.get("app.log"));       // true

五、Files API 核心操作详解

5.1 文件与目录操作

// 创建目录
Files.createDirectory(Paths.get("d:/feixiang/newDir"));       // 只创建最后一级
Files.createDirectories(Paths.get("d:/feixiang/a/b/c"));      // 创建所有中间目录

// 创建文件
Files.createFile(Paths.get("d:/feixiang/newFile.txt"));

// 复制
Files.copy(src, dest, StandardCopyOption.REPLACE_EXISTING);
Files.copy(src, dest, StandardCopyOption.COPY_ATTRIBUTES);

// 移动/重命名
Files.move(src, dest, StandardCopyOption.REPLACE_EXISTING);
Files.move(src, dest, StandardCopyOption.ATOMIC_MOVE);  // 原子移动

// 删除
Files.delete(path);             // 文件不存在时抛异常
Files.deleteIfExists(path);     // 文件不存在时返回false,不抛异常

5.2 文件属性

Path path = Paths.get("d:/feixiang/app.log");

// 基本属性
BasicFileAttributes attrs = Files.readAttributes(path, BasicFileAttributes.class);
attrs.size();              // 文件大小(字节)
attrs.creationTime();      // 创建时间
attrs.lastModifiedTime();  // 最后修改时间
attrs.isDirectory();       // 是否为目录
attrs.isRegularFile();     // 是否为常规文件
attrs.isSymbolicLink();    // 是否为符号链接

// 单个属性
long size = Files.size(path);
boolean exists = Files.exists(path);
boolean isDir = Files.isDirectory(path);
boolean readable = Files.isReadable(path);
boolean writable = Files.isWritable(path);

// 设置属性
Files.setLastModifiedTime(path, FileTime.fromMillis(System.currentTimeMillis()));

六、完整代码示例

示例1:飞翔科技日志文件递归遍历与分析

场景:运维部的服务器日志目录结构如下:d:/feixiang/logs/YYYY-MM/DD/app.log。小崔需要遍历所有日志文件,统计总大小和每个月份的文件数量。大翔教他用 Files.walkFileTree 实现。

import java.io.*;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.*;
import java.util.concurrent.atomic.AtomicLong;

/**
 * 飞翔科技 - 日志文件递归遍历与分析
 * 场景:遍历服务器日志目录,统计各月日志文件数量和总大小
 * 技术:Files.walkFileTree + SimpleFileVisitor
 * 作者:大翔
 */
public class FeixiangLogFileTraversal {

    // 统计容器
    private static final Map<String, FileStat> monthStats = new TreeMap<>();
    private static final AtomicLong totalSize = new AtomicLong(0);
    private static final AtomicLong totalFiles = new AtomicLong(0);

    public static void main(String[] args) throws IOException {
        // 1. 准备测试数据——创建模拟的日志目录结构
        prepareTestLogFiles();

        // 2. 使用 FileVisitor 遍历
        Path logRoot = Paths.get("d:/feixiang/logs");
        traverseLogs(logRoot);

        // 3. 打印统计报告
        printReport(logRoot);
    }

    /**
     * 准备模拟的测试日志文件
     */
    private static void prepareTestLogFiles() throws IOException {
        String[][] logs = {
            // {路径, 内容}
            {"2024-01/15/app.log", "INFO: 服务器启动 | 大翔登录"},
            {"2024-01/15/error.log", "ERROR: 数据库连接超时"},
            {"2024-01/20/app.log", "INFO: 备份完成"},
            {"2024-02/05/app.log", "INFO: 系统升级 | 白歌操作"},
            {"2024-02/05/debug.log", "DEBUG: 内存使用率 65%"},
            {"2024-02/14/app.log", "INFO: 情人节特别维护"},
            {"2024-02/14/access.log", "GET /api/users 200 15ms"},
            {"2024-03/01/app.log", "INFO: 月度报表生成"},
            {"2024-03/01/error.log", "WARN: 磁盘空间不足"},
            {"2024-03/15/app.log", "INFO: 服务器启动成功"},
        };

        for (String[] entry : logs) {
            Path filePath = Paths.get("d:/feixiang/logs", entry[0]);
            Files.createDirectories(filePath.getParent());
            Files.write(filePath, entry[1].getBytes("UTF-8"),
                    StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
        }
        System.out.println("[大翔] 测试日志文件已准备完成,共 " + logs.length + " 个文件\n");
    }

    /**
     * 核心:使用 Files.walkFileTree 递归遍历日志目录
     */
    public static void traverseLogs(Path startPath) throws IOException {
        System.out.println("[大翔] 开始遍历日志目录: " + startPath.toAbsolutePath());

        Files.walkFileTree(startPath, new SimpleFileVisitor<Path>() {

            /**
             * 进入目录前调用
             */
            @Override
            public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs)
                    throws IOException {
                System.out.println("[小崔] 进入目录: " + dir.getFileName());
                return FileVisitResult.CONTINUE;  // 继续遍历
            }

            /**
             * 访问到文件时调用
             */
            @Override
            public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
                    throws IOException {
                // 只处理 .log 文件
                if (!file.toString().endsWith(".log")) {
                    return FileVisitResult.CONTINUE;
                }

                // 解析月份 (从路径中提取 "YYYY-MM")
                String monthKey = extractMonth(file);
                if (monthKey == null) {
                    return FileVisitResult.CONTINUE;
                }

                // 更新统计
                long fileSize = attrs.size();
                totalSize.addAndGet(fileSize);
                totalFiles.incrementAndGet();

                monthStats.computeIfAbsent(monthKey, k -> new FileStat())
                          .addFile(file.getFileName().toString(), fileSize);

                System.out.printf("  [白歌] 发现日志: %s (%d bytes) [%s]%n",
                        file.getFileName(), fileSize, monthKey);

                return FileVisitResult.CONTINUE;
            }

            /**
             * 访问文件失败时调用(权限不足等)
             */
            @Override
            public FileVisitResult visitFileFailed(Path file, IOException exc)
                    throws IOException {
                System.err.println("[小崔] 无法访问文件: " + file + " - " + exc.getMessage());
                return FileVisitResult.CONTINUE;  // 继续遍历其他文件
            }

            /**
             * 离开目录后调用
             */
            @Override
            public FileVisitResult postVisitDirectory(Path dir, IOException exc)
                    throws IOException {
                System.out.println("[小崔] 离开目录: " + dir.getFileName());
                if (exc != null) {
                    System.err.println("[白歌] 目录遍历异常: " + exc.getMessage());
                }
                return FileVisitResult.CONTINUE;
            }
        });

        System.out.println("[大翔] 遍历完成!\n");
    }

    /**
     * 从文件路径中提取月份 (YYYY-MM)
     */
    private static String extractMonth(Path file) {
        // 路径格式: logs/2024-01/15/app.log
        Path parent = file.getParent();   // logs/2024-01/15
        if (parent != null) {
            Path month = parent.getParent();  // logs/2024-01
            if (month != null) {
                String monthName = month.getFileName().toString();
                if (monthName.matches("\\d{4}-\\d{2}")) {
                    return monthName;
                }
            }
        }
        return null;
    }

    /**
     * 打印统计报告
     */
    private static void printReport(Path logRoot) {
        System.out.println("========================================");
        System.out.println("   飞翔科技 日志文件统计报告");
        System.out.println("   根目录: " + logRoot.toAbsolutePath());
        System.out.println("========================================");
        System.out.printf("  总文件数: %d%n", totalFiles.get());
        System.out.printf("  总大小:   %.2f KB (%d bytes)%n",
                totalSize.get() / 1024.0, totalSize.get());
        System.out.println("----------------------------------------");

        for (Map.Entry<String, FileStat> entry : monthStats.entrySet()) {
            FileStat stat = entry.getValue();
            System.out.printf("  [%s] 文件数: %d, 大小: %d bytes%n",
                    entry.getKey(), stat.fileCount, stat.totalBytes);
        }
        System.out.println("========================================");
    }

    /**
     * 内部统计类
     */
    static class FileStat {
        int fileCount = 0;
        long totalBytes = 0;
        List<String> fileNames = new ArrayList<>();

        void addFile(String name, long bytes) {
            fileCount++;
            totalBytes += bytes;
            fileNames.add(name);
        }
    }
}

控制台输出:

[大翔] 测试日志文件已准备完成,共 10 个文件

[大翔] 开始遍历日志目录: d:\feixiang\logs
[小崔] 进入目录: logs
[小崔] 进入目录: 2024-01
[小崔] 进入目录: 15
  [白歌] 发现日志: app.log (42 bytes) [2024-01]
  [白歌] 发现日志: error.log (27 bytes) [2024-01]
[小崔] 离开目录: 15
[小崔] 进入目录: 20
  [白歌] 发现日志: app.log (20 bytes) [2024-01]
[小崔] 离开目录: 20
[小崔] 离开目录: 2024-01
[小崔] 进入目录: 2024-02
[小崔] 进入目录: 05
  [白歌] 发现日志: app.log (27 bytes) [2024-02]
  [白歌] 发现日志: debug.log (22 bytes) [2024-02]
[小崔] 离开目录: 05
[小崔] 进入目录: 14
  [白歌] 发现日志: app.log (24 bytes) [2024-02]
  [白歌] 发现日志: access.log (27 bytes) [2024-02]
[小崔] 离开目录: 14
[小崔] 离开目录: 2024-02
[小崔] 进入目录: 2024-03
[小崔] 进入目录: 01
  [白歌] 发现日志: app.log (21 bytes) [2024-03]
  [白歌] 发现日志: error.log (22 bytes) [2024-03]
[小崔] 离开目录: 01
[小崔] 进入目录: 15
  [白歌] 发现日志: app.log (22 bytes) [2024-03]
[小崔] 离开目录: 15
[小崔] 离开目录: 2024-03
[小崔] 离开目录: logs
[大翔] 遍历完成!

========================================
   飞翔科技 日志文件统计报告
   根目录: d:\feixiang\logs
========================================
  总文件数: 10
  总大小:   0.25 KB (254 bytes)
----------------------------------------
  [2024-01] 文件数: 3, 大小: 89 bytes
  [2024-02] 文件数: 4, 大小: 100 bytes
  [2024-03] 文件数: 3, 大小: 65 bytes
========================================

示例2:飞翔科技批量文件重命名与迁移工具

场景:白歌发现项目中的图片资源命名不规范——有的用空格,有的用中文,有的甚至没有扩展名。她写了一个工具,用 NIO.2 的 Files.move 和 Files.walk 批量重命名和整理。

import java.io.*;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * 飞翔科技 - 批量文件重命名与迁移工具
 * 场景:资源文件命名不规范,需要统一格式化
 * 功能:
 *   1. 递归遍历所有文件
 *   2. 去除文件名中的空格和特殊字符
 *   3. 统一扩展名为小写
 *   4. 按修改时间归档到分类目录
 * 作者:白歌
 */
public class FeixiangFileBatchRename {

    private static final String SOURCE_DIR = "d:/feixiang/assets_in";
    private static final String TARGET_DIR = "d:/feixiang/assets_out";

    // 重命名统计
    private static int renamedCount = 0;
    private static int copiedCount = 0;
    private static int skippedCount = 0;

    public static void main(String[] args) throws IOException {
        // 1. 准备测试文件
        prepareMessyFiles();

        // 2. 批量重命名(原地改名)
        batchRename(Paths.get(SOURCE_DIR));

        // 3. 按时间归档到目标目录
        archiveByMonth(Paths.get(SOURCE_DIR), Paths.get(TARGET_DIR));

        // 4. 对比两个目录
        compareDirectories();

        // 5. 打印报告
        printSummary();
    }

    /**
     * 准备命名混乱的测试文件
     */
    private static void prepareMessyFiles() throws IOException {
        // 清理并创建源目录
        deleteIfExists(Paths.get(SOURCE_DIR));
        deleteIfExists(Paths.get(TARGET_DIR));
        Files.createDirectories(Paths.get(SOURCE_DIR));
        Files.createDirectories(Paths.get(Paths.get(SOURCE_DIR), "subdir"));

        // 各种不规范命名
        Path[] messyFiles = {
            Paths.get(SOURCE_DIR, "大翔 的 照片.JPG"),        // 中文+空格
            Paths.get(SOURCE_DIR, "logo   final.png"),        // 多余空格
            Paths.get(SOURCE_DIR, "FEIXIANG-BANNER.PNG"),     // 大写扩展名
            Paths.get(SOURCE_DIR, "pic_2024_01_15"),           // 无扩展名
            Paths.get(SOURCE_DIR, "报表 -- 2024.xlsx"),        // 特殊字符
            Paths.get(SOURCE_DIR, "subdir", "Frank's Photo.jpg"), // 子目录 + 引号
            Paths.get(SOURCE_DIR, "subdir", "zhulu_resume.PDF"),  // 大写扩展名
            Paths.get(SOURCE_DIR, "subdir", "README.txt"),         // 正常命名
        };

        for (Path f : messyFiles) {
            Files.createDirectories(f.getParent());
            // 写入一些内容以创建文件
            Files.write(f, ("file: " + f.getFileName()).getBytes("UTF-8"),
                    StandardOpenOption.CREATE);
        }

        System.out.println("[白歌] 测试文件准备完成,共 " + messyFiles.length + " 个文件\n");
    }

    /**
     * 批量重命名:规范文件名
     * 规则:
     *  - 中文空格替换为下划线
     *  - 多个空格合并为一个下划线
     *  - 去除特殊字符(保留 中文/英文/数字/下划线/连字符/点)
     *  - 扩展名统一为小写
     */
    public static void batchRename(Path rootDir) throws IOException {
        System.out.println("========== 阶段一:批量重命名 ==========");

        try (Stream<Path> files = Files.walk(rootDir)) {
            List<Path> allFiles = files
                    .filter(Files::isRegularFile)
                    .collect(Collectors.toList());

            for (Path file : allFiles) {
                String oldName = file.getFileName().toString();
                String newName = normalizeFileName(oldName);

                if (!newName.equals(oldName)) {
                    Path newPath = file.resolveSibling(newName);
                    Files.move(file, newPath, StandardCopyOption.REPLACE_EXISTING);
                    renamedCount++;
                    System.out.printf("[小崔] 重命名: '%s' => '%s'%n", oldName, newName);
                } else {
                    skippedCount++;
                }
            }
        }
        System.out.println();
    }

    /**
     * 规范化文件名
     */
    private static String normalizeFileName(String fileName) {
        // 分离文件名和扩展名
        int dotIndex = fileName.lastIndexOf('.');
        String namePart, extPart;
        if (dotIndex > 0) {
            namePart = fileName.substring(0, dotIndex);
            extPart = "." + fileName.substring(dotIndex + 1).toLowerCase();
        } else {
            namePart = fileName;
            extPart = "";
        }

        // 去除特殊字符(保留中文、字母、数字、下划线、连字符)
        String cleaned = namePart.replaceAll("[^\\u4e00-\\u9fa5a-zA-Z0-9_\\-\\s]", "");
        // 多余空格合并为单个下划线
        cleaned = cleaned.replaceAll("\\s+", "_");
        // 多个下划线合并
        cleaned = cleaned.replaceAll("_+", "_");
        // 去除首尾下划线
        cleaned = cleaned.replaceAll("^_|_$", "");

        // 如果清理后为空,使用默认名
        if (cleaned.isEmpty()) {
            cleaned = "unnamed";
        }

        return cleaned + extPart;
    }

    /**
     * 按修改月份归档到目标目录
     */
    public static void archiveByMonth(Path srcDir, Path destDir) throws IOException {
        System.out.println("========== 阶段二:按时间归档 ==========");

        Files.createDirectories(destDir);

        try (Stream<Path> files = Files.walk(srcDir)) {
            files.filter(Files::isRegularFile).forEach(file -> {
                try {
                    BasicFileAttributes attrs =
                            Files.readAttributes(file, BasicFileAttributes.class);
                    // 获取修改时间的年月
                    FileTime modTime = attrs.lastModifiedTime();
                    String yearMonth = modTime.toString().substring(0, 7); // "2024-03"

                    // 目标子目录
                    Path monthDir = destDir.resolve(yearMonth);
                    Files.createDirectories(monthDir);

                    // 目标文件路径
                    Path destFile = monthDir.resolve(file.getFileName());
                    Files.copy(file, destFile, StandardCopyOption.REPLACE_EXISTING);
                    copiedCount++;
                    System.out.printf("[白歌] 归档: %s => %s%n",
                            file.getFileName(), destFile);

                } catch (IOException e) {
                    System.err.println("[小崔] 归档失败: " + file + " - " + e.getMessage());
                }
            });
        }
        System.out.println();
    }

    /**
     * 对比源目录和目标目录的内容
     */
    private static void compareDirectories() throws IOException {
        System.out.println("========== 阶段三:目录对比 ==========");

        System.out.println("[Frank] 源目录 (整理后):");
        try (Stream<Path> files = Files.walk(Paths.get(SOURCE_DIR))) {
            files.filter(Files::isRegularFile)
                 .forEach(f -> System.out.println("  " + f.getFileName()));
        }

        System.out.println("\n[Frank] 目标目录 (归档后):");
        try (Stream<Path> files = Files.walk(Paths.get(TARGET_DIR))) {
            files.filter(Files::isRegularFile)
                 .forEach(f -> System.out.println("  " + f));
        }
        System.out.println();
    }

    private static void printSummary() {
        System.out.println("========================================");
        System.out.println("   飞翔科技 批量文件处理报告");
        System.out.println("========================================");
        System.out.printf("  重命名: %d 个文件%n", renamedCount);
        System.out.printf("  归档:   %d 个文件%n", copiedCount);
        System.out.printf("  跳过:   %d 个文件 (名称已规范)%n", skippedCount);
        System.out.printf("  源目录: %s%n", SOURCE_DIR);
        System.out.printf("  目标目录: %s%n", TARGET_DIR);
        System.out.println("========================================");
    }

    private static void deleteIfExists(Path dir) throws IOException {
        if (Files.exists(dir)) {
            try (Stream<Path> files = Files.walk(dir)) {
                files.sorted(Comparator.reverseOrder()) // 先删文件再删目录
                     .forEach(f -> {
                         try { Files.deleteIfExists(f); }
                         catch (IOException e) { /* ignore */ }
                     });
            }
        }
    }
}

控制台输出:

[白歌] 测试文件准备完成,共 8 个文件

========== 阶段一:批量重命名 ==========
[小崔] 重命名: '大翔 的 照片.JPG' => '大翔_的照片.jpg'
[小崔] 重命名: 'logo   final.png' => 'logo_final.png'
[小崔] 重命名: 'FEIXIANG-BANNER.PNG' => 'FEIXIANG-BANNER.png'
[小崔] 重命名: '报表 -- 2024.xlsx' => '报表_2024.xlsx'
[小崔] 重命名: 'Frank's Photo.jpg' => 'Franks_Photo.jpg'
[小崔] 重命名: 'zhulu_resume.PDF' => 'zhulu_resume.pdf'

========== 阶段二:按时间归档 ==========
[白歌] 归档: 大翔_的照片.jpg => d:\feixiang\assets_out\2024-03\大翔_的照片.jpg
[白歌] 归档: logo_final.png => d:\feixiang\assets_out\2024-03\logo_final.png
[白歌] 归档: FEIXIANG-BANNER.png => d:\feixiang\assets_out\2024-03\FEIXIANG-BANNER.png
[白歌] 归档: pic_2024_01_15 => d:\feixiang\assets_out\2024-03\pic_2024_01_15
[白歌] 归档: 报表_2024.xlsx => d:\feixiang\assets_out\2024-03\报表_2024.xlsx
[白歌] 归档: Franks_Photo.jpg => d:\feixiang\assets_out\2024-03\Franks_Photo.jpg
[白歌] 归档: zhulu_resume.pdf => d:\feixiang\assets_out\2024-03\zhulu_resume.pdf
[白歌] 归档: README.txt => d:\feixiang\assets_out\2024-03\README.txt

========== 阶段三:目录对比 ==========
[Frank] 源目录 (整理后):
  大翔_的照片.jpg
  logo_final.png
  FEIXIANG-BANNER.png
  pic_2024_01_15
  报表_2024.xlsx
  Franks_Photo.jpg
  zhulu_resume.pdf
  README.txt

[Frank] 目标目录 (归档后):
  d:\feixiang\assets_out\2024-03\大翔_的照片.jpg
  d:\feixiang\assets_out\2024-03\logo_final.png
  ... (省略)

========================================
   飞翔科技 批量文件处理报告
========================================
  重命名: 6 个文件
  归档:   8 个文件
  跳过:   2 个文件 (名称已规范)
  源目录: d:/feixiang/assets_in
  目标目录: d:/feixiang/assets_out
========================================

示例3:Files 便捷方法大集合 — Path vs File 对照

场景:朱璐还在用老式File API写代码,Frank建议她切换到Path。大翔写了对照示例展示Path的优雅之处。

import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.util.List;
import java.util.stream.Stream;

/**
 * 飞翔科技 - Path vs File 对比示例
 * 场景:对比 JDK 7 NIO.2 与传统 IO 的文件操作方式
 * 作者:大翔 & Frank
 */
public class PathVsFileComparison {

    public static void main(String[] args) throws IOException {
        System.out.println("========== 飞翔科技 Path vs File 对比 ==========\n");

        compare_CreateAndDelete();
        compare_ReadWrite();
        compare_ListFiles();
        compare_CopyAndMove();
        compare_FileAttributes();
    }

    /**
     * 对比1:创建和删除操作
     */
    private static void compare_CreateAndDelete() throws IOException {
        System.out.println("--- 创建与删除 ---");

        // ❌ 老式 File API
        File oldDir = new File("d:/feixiang/comparison/file_way");
        boolean created = oldDir.mkdirs();  // 返回boolean,失败时不知道原因
        System.out.println("[朱璐] File.mkdirs: " + created);
        File oldFile = new File(oldDir, "test.txt");
        oldFile.createNewFile();  // 返回boolean

        // ✅ NIO.2 Path API
        Path newDir = Paths.get("d:/feixiang/comparison/path_way");
        Path result = Files.createDirectories(newDir);  // 返回创建的路径
        System.out.println("[Frank] Files.createDirectories: " + result);
        Path newFile = newDir.resolve("test.txt");
        Files.createFile(newFile);  // 文件已存在抛 FileAlreadyExistsException

        // 删除清理
        Files.delete(newFile);       // 文件不存在抛异常
        Files.deleteIfExists(newFile); // 无异常版本
        oldFile.delete();
    }

    /**
     * 对比2:读写文件
     */
    private static void compare_ReadWrite() throws IOException {
        System.out.println("\n--- 文件读写 ---");

        Path file = Paths.get("d:/feixiang/comparison/readwrite.txt");
        Files.createDirectories(file.getParent());

        // ✅ NIO.2: 一行读取所有行
        Files.write(file, 
            java.util.Arrays.asList("飞翔科技", "Feixiang Technology", "2024"),
            StandardCharsets.UTF_8);

        List<String> lines = Files.readAllLines(file, StandardCharsets.UTF_8);
        System.out.println("[白歌] readAllLines: " + lines);

        // ✅ NIO.2: 一次性读取所有字节
        byte[] allBytes = Files.readAllBytes(file);
        System.out.println("[大翔] readAllBytes length: " + allBytes.length);

        // ✅ NIO.2: 使用 Stream 逐行处理(大文件友好)
        try (Stream<String> lineStream = Files.lines(file, StandardCharsets.UTF_8)) {
            long count = lineStream
                    .filter(l -> l.contains("飞翔"))
                    .count();
            System.out.println("[Frank] lines().filter 包含'飞翔'的行数: " + count);
        }

        // ✅ NIO.2: newBufferedReader / newBufferedWriter 与传统方式对比
        // 传统方式:
        //   BufferedReader br = new BufferedReader(new InputStreamReader(
        //       new FileInputStream("file"), "UTF-8"));
        // NIO.2方式(更简洁):
        try (BufferedReader br = Files.newBufferedReader(file, StandardCharsets.UTF_8)) {
            String firstLine = br.readLine();
            System.out.println("[白歌] newBufferedReader 第一行: " + firstLine);
        }

        // 清理
        Files.deleteIfExists(file);
    }

    /**
     * 对比3:列出目录
     */
    private static void compare_ListFiles() throws IOException {
        System.out.println("\n--- 目录列表 ---");

        Path dir = Paths.get("d:/feixiang/comparison/list_test");
        Files.createDirectories(dir);
        Files.createFile(dir.resolve("a.txt"));
        Files.createFile(dir.resolve("b.txt"));
        Files.createFile(dir.resolve("c.log"));
        Files.createDirectory(dir.resolve("subdir"));

        // ❌ File: listFiles 返回 File[],需要写循环
        File oldDir = dir.toFile();
        File[] files = oldDir.listFiles();
        if (files != null) {
            for (File f : files) {
                System.out.println("[朱璐] File.listFiles: " + f.getName());
            }
        }

        // ✅ NIO.2: Files.list 返回 Stream<Path>(try-with-resources保证关闭)
        System.out.println("--- 仅列出 .txt 文件 ---");
        try (Stream<Path> stream = Files.list(dir)) {
            stream.filter(p -> p.toString().endsWith(".txt"))
                  .forEach(p -> System.out.println(
                          "[Frank] Files.list.filter: " + p.getFileName()));
        }

        // ✅ NIO.2: Files.walk 返回 Stream(递归,限制深度)
        System.out.println("--- 递归遍历(深度2) ---");
        try (Stream<Path> stream = Files.walk(dir, 2)) {
            stream.forEach(p -> System.out.println(
                    "[大翔] Files.walk: " + p.getFileName()));
        }

        // 清理
        Files.walk(dir)
             .sorted(java.util.Comparator.reverseOrder())
             .forEach(p -> {
                 try { Files.deleteIfExists(p); }
                 catch (IOException e) { /* ignore */ }
             });
    }

    /**
     * 对比4:复制和移动
     */
    private static void compare_CopyAndMove() throws IOException {
        System.out.println("\n--- 复制与移动 ---");

        Path workDir = Paths.get("d:/feixiang/comparison/cmtest");
        Files.createDirectories(workDir);

        Path src = workDir.resolve("source.txt");
        Files.write(src, "飞翔科技源文件".getBytes(StandardCharsets.UTF_8));

        // ✅ NIO.2: Files.copy
        Path dest = workDir.resolve("copy_target.txt");
        Files.copy(src, dest, StandardCopyOption.REPLACE_EXISTING);
        System.out.println("[白歌] Files.copy: " + Files.exists(dest));

        // ✅ NIO.2: Files.move (相当于重命名)
        Path moved = workDir.resolve("moved_target.txt");
        Files.move(dest, moved, StandardCopyOption.REPLACE_EXISTING);
        System.out.println("[大翔] Files.move: copy_target.txt -> moved_target.txt, "
                + "原文件存在=" + Files.exists(dest));

        // ❌ File: renameTo 不可靠(跨文件系统可能失败、没有异常信息)
        File oldFile = new File(workDir.toFile(), "old.txt");
        oldFile.createNewFile();
        File newFile = new File(workDir.toFile(), "new.txt");
        boolean renamed = oldFile.renameTo(newFile);
        System.out.println("[朱璐] File.renameTo: " + renamed
                + " (无异常信息,失败也返回false)");

        // 清理
        Files.deleteIfExists(src);
        Files.deleteIfExists(moved);
        Files.deleteIfExists(newFile.toPath());
        Files.deleteIfExists(workDir);
    }

    /**
     * 对比5:文件属性
     */
    private static void compare_FileAttributes() throws IOException {
        System.out.println("\n--- 文件属性 ---");

        Path file = Paths.get("d:/feixiang/comparison/attr_test.txt");
        Files.createDirectories(file.getParent());
        Files.write(file, "test".getBytes(StandardCharsets.UTF_8));

        // ✅ NIO.2: 一次性读取所有基本属性
        BasicFileAttributes attrs = Files.readAttributes(file, BasicFileAttributes.class);
        System.out.println("[Frank] BasicFileAttributes:");
        System.out.println("  size: " + attrs.size());
        System.out.println("  isDirectory: " + attrs.isDirectory());
        System.out.println("  creationTime: " + attrs.creationTime());
        System.out.println("  lastModifiedTime: " + attrs.lastModifiedTime());

        // ✅ NIO.2: Files 直接提供的便捷方法
        long size = Files.size(file);
        boolean exists = Files.exists(file);
        System.out.println("[大翔] Files.size: " + size);
        System.out.println("[大翔] Files.exists: " + exists);

        // ❌ File: 每次属性查询是一个系统调用
        File oldFile = file.toFile();
        System.out.println("[朱璐] File.length: " + oldFile.length());
        System.out.println("[朱璐] File.exists: " + oldFile.exists());
        System.out.println("[朱璐] File.lastModified: " + oldFile.lastModified());

        Files.deleteIfExists(file);
        Files.deleteIfExists(file.getParent());
    }
}

控制台输出(部分):

========== 飞翔科技 Path vs File 对比 ==========

--- 创建与删除 ---
[朱璐] File.mkdirs: true
[Frank] Files.createDirectories: d:\feixiang\comparison\path_way

--- 文件读写 ---
[白歌] readAllLines: [飞翔科技, Feixiang Technology, 2024]
[大翔] readAllBytes length: 38
[Frank] lines().filter 包含'飞翔'的行数: 1
[白歌] newBufferedReader 第一行: 飞翔科技

--- 目录列表 ---
[朱璐] File.listFiles: a.txt
[朱璐] File.listFiles: b.txt
[朱璐] File.listFiles: c.log
[朱璐] File.listFiles: subdir
--- 仅列出 .txt 文件 ---
[Frank] Files.list.filter: a.txt
[Frank] Files.list.filter: b.txt
--- 递归遍历(深度2) ---
[大翔] Files.walk: list_test
[大翔] Files.walk: a.txt
[大翔] Files.walk: b.txt
[大翔] Files.walk: c.log
[大翔] Files.walk: subdir

--- 复制与移动 ---
[白歌] Files.copy: true
[大翔] Files.move: copy_target.txt -> moved_target.txt, 原文件存在=false
[朱璐] File.renameTo: true (无异常信息,失败也返回false)

--- 文件属性 ---
[Frank] BasicFileAttributes:
  size: 4
  isDirectory: false
  creationTime: 2024-03-15T10:30:45Z
  lastModifiedTime: 2024-03-15T10:30:45Z
[大翔] Files.size: 4
[大翔] Files.exists: true
[朱璐] File.length: 4
[朱璐] File.exists: true
[朱璐] File.lastModified: 1710502245000

六、易错场景

易错1:Files.walk / Files.list 返回的 Stream 忘记关闭

// ❌ 错误:Stream 未关闭导致资源泄漏
Stream<Path> files = Files.walk(Paths.get("d:/feixiang"));
files.filter(Files::isRegularFile)
     .forEach(System.out::println);
// Stream 打开的文件描述符/目录句柄没有释放!

// ✅ 正确:使用 try-with-resources
try (Stream<Path> files = Files.walk(Paths.get("d:/feixiang"))) {
    files.filter(Files::isRegularFile)
         .forEach(System.out::println);
}  // 自动关闭 Stream,释放底层资源

原因:Files.walk() 和 Files.list() 返回的 Stream 包装了底层的 DirectoryStream,必须关闭才能释放目录句柄。在Windows上,未关闭的目录句柄会导致该目录无法被删除。

易错2:Files.delete() vs Files.deleteIfExists()

// ❌ 错误:盲目使用 Files.delete()
Files.delete(Paths.get("d:/feixiang/may_not_exist.txt"));
// 文件不存在 → NoSuchFileException!
// 目录非空 → DirectoryNotEmptyException!

// ✅ 正确:先检查或使用 deleteIfExists
Path path = Paths.get("d:/feixiang/may_not_exist.txt");
Files.deleteIfExists(path);  // 文件不存在仅返回false,不抛异常

// ✅ 删除非空目录的正确方式
Path dir = Paths.get("d:/feixiang/non_empty_dir");
try (Stream<Path> files = Files.walk(dir)) {
    files.sorted(Comparator.reverseOrder())  // 深度优先,先删文件后删目录
         .forEach(p -> {
             try {
                 Files.delete(p);
             } catch (IOException e) {
                 throw new UncheckedIOException(e);
             }
         });
}

易错3:Paths.get() 在不同OS上的路径分隔符差异

// ❌ 硬编码 Windows 路径分隔符
Path winPath = Paths.get("d:\\feixiang\\logs\\app.log");  // 仅Windows可用

// ❌ 硬编码 Unix 路径分隔符
Path unixPath = Paths.get("/opt/feixiang/logs/app.log");   // 仅Linux/Mac可用

// ✅ 使用可变参数或 File.separator
Path path1 = Paths.get("d:", "feixiang", "logs", "app.log");  // 跨平台
Path path2 = Paths.get(System.getProperty("user.dir"), "logs", "app.log");
Path path3 = FileSystems.getDefault().getPath("d:", "feixiang", "logs");

// 或者使用 resolve 拼接
Path base = Paths.get("d:/feixiang");
Path full = base.resolve("logs").resolve("app.log");

易错4:readAllLines 用于大文件导致 OOM

// ❌ 错误:2GB的日志文件用 readAllLines → OutOfMemoryError
List<String> lines = Files.readAllLines(Paths.get("d:/huge_server.log"));
for (String line : lines) {
    processLine(line);
}

// ✅ 方案1:使用 Files.lines() 返回 Stream(惰性读取)
try (Stream<String> lines = Files.lines(Paths.get("d:/huge_server.log"))) {
    lines.filter(l -> l.contains("ERROR"))
         .forEach(System.out::println);
}

// ✅ 方案2:使用 newBufferedReader 逐行读取
try (BufferedReader br = Files.newBufferedReader(
        Paths.get("d:/huge_server.log"), StandardCharsets.UTF_8)) {
    String line;
    while ((line = br.readLine()) != null) {
        processLine(line);
    }
}

易错5:混淆 resolve 和 resolveSibling

Path path = Paths.get("d:/feixiang/logs/app.log");

// resolve: 在路径末尾追加 → d:/feixiang/logs/app.log/data
Path r1 = path.resolve("data");

// resolveSibling: 替换路径最后一段 → d:/feixiang/logs/error.log
Path r2 = path.resolveSibling("error.log");

// 常见错误:想拼接到目录层级,却用了 resolve
Path dir = Paths.get("d:/feixiang/logs");  // 这是一个目录
Path file = dir.resolve("app.log");         // d:/feixiang/logs/app.log ✅

Path filePath = Paths.get("d:/feixiang/logs/app.log");  // 这是一个文件路径
Path wrong = filePath.resolve("error.log");  // d:/feixiang/logs/app.log/error.log ❌
Path correct = filePath.resolveSibling("error.log");     // d:/feixiang/logs/error.log ✅

七、面试考点

Q1: java.io.File 和 java.nio.file.Path 有什么区别?为什么要用Path?

答:

维度FilePath
引入版本JDK 1.0JDK 7
错误处理返回 boolean抛出具体异常
不可变性可变不可变
方法丰富度有限的路径操作丰富的路径拼接、解析、比较方法
符号链接不支持支持
文件属性有限丰富的属性视图
与Stream集成无walk/list/find/lines 返回Stream
跨文件系统renameTo 不可靠move/copy 跨文件系统可靠

追问:什么时候还需要用File?——与遗留代码交互、某些第三方库参数要求File类型时。可以通过 Path.toFile() 和 File.toPath() 互转。

Q2: Files.walk() 和 Files.walkFileTree() 有什么区别?

答:

  • Files.walk():返回 Stream<Path>,惰性遍历。适合过滤、收集等Stream操作。最大深度可配(默认 Integer.MAX_VALUE)。需在 try-with-resources 中使用。
  • Files.walkFileTree():使用访问者模式(FileVisitor)。在进入/离开目录、访问文件时有回调,可以在遍历过程中执行复杂逻辑,可以随时用 FileVisitResult.TERMINATE 终止,可以 SKIP_SUBTREE 跳过子树。

选择建议:

  • 简单的过滤+收集 → Files.walk()
  • 需要知道进入/离开目录事件、需要跳过子树、需要处理访问失败 → Files.walkFileTree()

Q3: Files.copy 的三个 StandardCopyOption 有什么区别?

答:

  • REPLACE_EXISTING:目标文件存在时替换。不指定时,目标存在会抛 FileAlreadyExistsException。
  • COPY_ATTRIBUTES:复制文件的最后修改时间等元数据到目标文件(尽力而为,不是所有文件系统都支持)。
  • ATOMIC_MOVE:Files.move() 专用,确保移动是原子操作——要么完全成功,要么完全不变。对于保证数据一致性重要(如防止进程崩溃时文件半移动)。不是所有文件系统都支持此选项,不支持时抛 AtomicMoveNotSupportedException。

Q4: 如何用NIO.2高效读取一个超大日志文件?

答:

// ❌ 不推荐:readAllLines → OOM
// ❌ 不推荐:readAllBytes → OOM

// ✅ 最佳实践:Files.lines() 惰性读取 + Stream处理
try (Stream<String> lines = Files.lines(
        Paths.get("d:/huge.log"), StandardCharsets.UTF_8)) {
    // 统计ERROR行数而不加载全部到内存
    long errorCount = lines.filter(l -> l.contains("ERROR")).count();
    System.out.println("ERROR count: " + errorCount);
}

// ✅ 备选:Files.newBufferedReader 传统方式
try (BufferedReader br = Files.newBufferedReader(
        Paths.get("d:/huge.log"), StandardCharsets.UTF_8)) {
    String line;
    int lineNum = 0;
    while ((line = br.readLine()) != null) {
        if (lineNum++ % 100000 == 0) {
            System.out.println("Processing line " + lineNum);
        }
    }
}

进阶:还可以使用 FileChannel(NIO核心通道)配合 MappedByteBuffer 进行内存映射文件访问,实现零拷贝读取。

Q5: NIO.2 中的 Path 为什么设计成不可变的?

答:

  1. 线程安全:不可变对象天然线程安全,无需同步。
  2. 避免副作用:类似 String,resolve()、normalize() 等操作返回新实例,不改变原对象,避免在共享引用时产生意外的副作用。
  3. 可缓存/Hash友好:不可变对象可以作为Map的Key使用,hashCode不会变化。
  4. 设计一致性:JDK 7 同时引入的 java.time 包也采用了不可变设计,这是现代Java API的设计趋势。

八、JDK 8 NIO 补充:Stream 集成

JDK 8 为 Files 类新增了返回 Stream 的方法,与 Lambda/Stream API 无缝集成:

// Files.list: 列出当前目录(非递归)
try (Stream<Path> entries = Files.list(Paths.get("."))) {
    entries.filter(Files::isRegularFile)
           .sorted(Comparator.comparingLong(p -> {
               try { return Files.size(p); }
               catch (IOException e) { return 0L; }
           }))
           .forEach(System.out::println);
}

// Files.find: 按条件搜索(递归)
try (Stream<Path> matches = Files.find(
        Paths.get("d:/feixiang"),
        5,  // 最大深度
        (path, attrs) -> attrs.isRegularFile()
                && path.toString().endsWith(".log")
                && attrs.size() > 1024)) {
    matches.forEach(System.out::println);
}

// Files.lines: 读取文件所有行作为 Stream
try (Stream<String> lines = Files.lines(
        Paths.get("d:/feixiang/data.csv"), StandardCharsets.UTF_8)) {
    List<String> headers = lines.skip(0).limit(1)
            .flatMap(line -> Stream.of(line.split(",")))
            .collect(Collectors.toList());
    System.out.println("CSV Headers: " + headers);
}

九、最佳实践总结

实践说明
新代码优先使用 Path,而非 FilePath 不可变、方法丰富、错误处理优秀
使用 try-with-resources 包裹 Files.walk/list/lines避免资源泄漏(目录句柄/文件句柄)
大文件用 Files.lines() 而非 readAllLines避免 OOM,惰性读取
使用 StandardCharsets 常量编译期安全,避免 UnsupportedEncodingException
跨平台时用可变参数 Paths.get()Paths.get("a", "b", "c") 而非字符串拼接
显式指定删除/复制选项REPLACE_EXISTING 等选项使意图明确
文件遍历注意符号链接循环FOLLOW_LINKS 选项可能导致无限循环
批量操作使用 Stream 而非循环代码简洁,支持并行处理

大翔合上电脑:"JDK 7 的 NIO.2 是 Java IO 史上最大的革新之一。Path、Files、FileVisitor 这套API,把以前各种繁琐的文件操作变得优雅简洁。再配合 JDK 8 的 Stream,文件处理代码可以写得像诗一样。"白歌点头:"不过面试的时候,至少要能说清楚 File 和 Path 的区别、walk 和 walkFileTree 的区别、以及 readAllLines 为什么不适合大文件。"Frank 远程语音里传来笑声:"My rule of thumb — if you're still typing 'new File()' in a new project, you're doing it wrong. Long live java.nio.file!" 小崔看着笔记:"原来文件操作还能这么写!之前用File的递归遍历简直是受罪……"朱璐已经开始重构她的代码了。

上一页
序列化 详解
下一页
NIO与Files工具类