Java NIO

一、什么是NIO.2

1.1 NIO.2基本概念

NIO.2 是 Java 7 引入的新 I/O API,位于 java.nio.file包中。它是对传统 java.io.File的全面升级,提供了更强大、更灵活的文件系统操作功能。
主要改进:

1)更好的异常处理
2)更丰富的文件操作
3)支持符号链接
4)文件属性访问
5)目录遍历和监控
6)异步 I/O 操作

1.2 NIO.2 核心基础接口总结

组件 作用
Path 替代 java.io.File,表示文件 / 目录路径(核心)
Paths 工具类,创建 Path 对象
Files 工具类,提供所有文件操作(创建、复制、删除、读写等)
FileSystem 文件系统抽象,支持自定义文件系统
FileVisitor 文件树遍历接口
WatchService 文件系统监听服务
AsynchronousFileChannel 异步文件通道
AsynchronousSocketChannel 异步网络套接字通道

二、核心类和接口

2.1 Path接口

1. 创建Path接口对象
(最常用)
Path path1 = Paths.get("D:/test.txt");
Path path2 = Paths.get("D:", "test", "demo.txt");

2. 常用方法
Path path = Paths.get("D:/a/b/c/test.txt");

// 获取路径信息
path.getFileName();       // test.txt(文件名)
path.getParent();         // D:\a\b\c(父目录)
path.getRoot();           // D:\(根路径)
path.getNameCount();      // 4(a,b,c,test.txt)
path.getName(1);          // b(索引从0开始)

// 路径解析/拼接
Path p1 = Paths.get("D:/a");
Path p2 = p1.resolve("b/test.txt"); // D:\a\b\test.txt

// 相对路径
path.relativize(Paths.get("D:/a")); // 相对路径

// 标准化路径(去除 ../ ./)
path.normalize();

// 判断是否绝对路径
path.isAbsolute();

2.2 Path接口具体实例

Path是 NIO.2 的核心,代表文件系统中的路径。

import java.nio.file.Path;
import java.nio.file.Paths;

public class PathExample {
    public static void main(String[] args) {
        // 创建Path对象的几种方式
        Path path1 = Paths.get("/home/user/test.txt"); // Linux/ Mac系统的绝对路径,创建 Path 对象
        Path path2 = Paths.get("C:", "Users", "test", "file.txt"); // Windows 绝对路径,分段传入,自动拼接
        Path path3 = Paths.get("relative/path/to/file.txt"); //相对路径 相对于 项目当前运行目录
        
        // Path常用方法
        System.out.println("文件名: " + path1.getFileName());
        System.out.println("父目录: " + path1.getParent());
        System.out.println("根目录: " + path1.getRoot());
        System.out.println("绝对路径: " + path1.toAbsolutePath());
        System.out.println("标准化路径: " + path1.normalize());
        
        // 路径操作
        Path base = Paths.get("/base");
        Path resolved = base.resolve("subdir/file.txt");
        Path relativized = Paths.get("/a/b/c").relativize(Paths.get("/a/b/c/d/e"));
    }
}

小小解释:

1、Path       = 文件/目录路径(接口)
2、Paths.get() = 静态方法,用来创建 Path 对象
3、resolve() → 拼接路径(把子路径拼到父路径后面)
从 父 → 子
如:
Path base = Paths.get("/base");
// 拼接
Path resolved = base.resolve("subdir/file.txt");
得到结果:/base/subdir/file.txt
 
4、relativize() → 计算相对路径(从当前路径走到目标路径怎么走)
从 某路径 → 目标路径
如下案例:
Path relativized = Paths.get("/a/b/c").relativize(Paths.get("/a/b/c/d/e"));
从 /a/b/c 出发,怎么走才能到达 /a/b/c/d/e?
结果:d/e

2.2 Paths工具类

Paths是创建 Path对象的工厂类。

// 创建Path的等价方式
Path p1 = Paths.get("/tmp/foo");
Path p2 = FileSystems.getDefault().getPath("/tmp/foo");

2.3 Files工具类

Files是 NIO.2 最重要的工具类,提供了大量静态方法。

import java.nio.file.Files;
import java.nio.file.StandardCopyOption;
import java.nio.file.attribute.PosixFilePermissions;
import java.util.List;

public class FilesExample {
    public static void basicOperations() throws Exception {
        Path path = Paths.get("test.txt");
        
        // 检查文件状态
        boolean exists = Files.exists(path);
        boolean isDirectory = Files.isDirectory(path);
        boolean isRegularFile = Files.isRegularFile(path);
        boolean isReadable = Files.isReadable(path);
        boolean isWritable = Files.isWritable(path);
        boolean isExecutable = Files.isExecutable(path);
        boolean isHidden = Files.isHidden(path);
        
        // 比较两个文件
        boolean isSameFile = Files.isSameFile(path1, path2);
    }
}

三、文件操作详情

3.1 创建文件和目录

// 创建目录(多级目录用 createDirectories)
Files.createDirectory(Paths.get("D:/test"));
Files.createDirectories(Paths.get("D:/a/b/c")); // 递归创建

// 创建空文件
Files.createFile(Paths.get("D:/test.txt"));

// 创建临时文件/目录
Path tempFile = Files.createTempFile("tmp_", ".txt");
Path tempDir = Files.createTempDirectory("tmp_dir_");

// 判断是否存在
Files.exists(path);
Files.notExists(path);

// 判断是否是文件/目录
Files.isRegularFile(path);
Files.isDirectory(path);

// 获取文件大小(字节)
long size = Files.size(path);

// 获取最后修改时间
FileTime lastModified = Files.getLastModifiedTime(path);

如下实例:

import java.nio.file.*;
import java.util.HashSet;
import java.util.Set;

public class CreateOperations {
    public static void main(String[] args) throws Exception {
        // 1. 创建文件
        Path newFile = Paths.get("newfile.txt");
        Files.createFile(newFile);
        
        // 创建文件时指定权限(UNIX系统)
        
        Set<PosixFilePermission> perms = PosixFilePermissions.fromString("rw-r-----"); // 把权限字符串变成权限对象
        FileAttribute<Set<PosixFilePermission>> attr = 
            PosixFilePermissions.asFileAttribute(perms); // 包装成系统能识别的格式
        Files.createFile(newFile, attr); // 创建文件同时设置权限
        
        // 2. 创建单级目录
        Path dir = Paths.get("newdir");
        Files.createDirectory(dir);
        
        // 3. 创建多级目录
        Path nestedDir = Paths.get("a/b/c/d");
        Files.createDirectories(nestedDir);
        
        // 4. 创建临时文件
        Path tempFile = Files.createTempFile("prefix", ".txt");
        Path tempFileWithDir = Files.createTempFile(dir, "prefix", ".txt");
        
        // 5. 创建临时目录
        Path tempDir = Files.createTempDirectory("tempDir");
    }
}

3.2 复制、移动和删除文件

Path src = Paths.get("src.txt");
Path dest = Paths.get("dest.txt");

// 复制文件(可选参数:覆盖、保留属性等)
Files.copy(src, dest, StandardCopyOption.REPLACE_EXISTING);

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

// 删除文件(文件不存在抛异常)
Files.delete(dest);
// 安全删除(不存在不抛异常)
Files.deleteIfExists(dest);
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.io.IOException;

public class FileOperations {
    public static void copyFile() throws IOException {
        Path source = Paths.get("source.txt");
        Path target = Paths.get("target.txt");
        
        // 基本复制
        Files.copy(source, target);
        
        // 带选项的复制
        Files.copy(source, target, 
            StandardCopyOption.REPLACE_EXISTING,  // 覆盖已存在文件
            StandardCopyOption.COPY_ATTRIBUTES,    // 复制文件属性
            LinkOption.NOFOLLOW_LINKS);           // 不跟随符号链接
    }
    
    public static void moveFile() throws IOException {
        Path source = Paths.get("oldname.txt");
        Path target = Paths.get("newname.txt");
        
        // 移动/重命名
        Files.move(source, target, StandardCopyOption.REPLACE_EXISTING);
        
        // 原子移动(如果文件系统支持)
        Files.move(source, target, StandardCopyOption.ATOMIC_MOVE);
    }
    
    public static void deleteFile() throws IOException {
        Path path = Paths.get("todelete.txt");
        
        // 删除文件
        Files.delete(path);
        
        // 安全删除(文件不存在时不抛异常)
        Files.deleteIfExists(path);
    }
    
    public static void copyDirectory() throws IOException {
        Path sourceDir = Paths.get("sourceDir");
        Path targetDir = Paths.get("targetDir");
        
        // 复制整个目录
        Files.walkFileTree(sourceDir, new SimpleFileVisitor<Path>() {
            @Override
            public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) 
                    throws IOException {
                Path target = targetDir.resolve(sourceDir.relativize(dir));
                Files.createDirectories(target);
                return FileVisitResult.CONTINUE;
            }
            
            @Override
            public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) 
                    throws IOException {
                Path target = targetDir.resolve(sourceDir.relativize(file));
                Files.copy(file, target, StandardCopyOption.REPLACE_EXISTING);
                return FileVisitResult.CONTINUE;
            }
        });
    }
}

关于以上最后这段java复制整个目录的标准模板,是非常重要的。

1. 核心内容
1)Files.walkFileTree
遍历目录树(递归进入所有子文件夹)
2)SimpleFileVisitor
  目录遍历器,你可以监听:
  进入目录前
  访问文件
  访问失败等
3)resolve + relativize
计算目标路径(复制到哪里)
4)new SimpleFileVisitor<Path>() 创建一个目录遍历处理器
5)preVisitDirectory(...) 进入目录前自动调用 → 用来创建目标文件夹
6)return FileVisitResult.CONTINUE 遍历继续,不要停

3.3 文件读写操作

NIO.2 提供一行代码读写文件,无需手动关闭流。
1)读取文件
// 读取所有行(返回 List<String>)
List<String> lines = Files.readAllLines(Paths.get("test.txt"), StandardCharsets.UTF_8);

// 读取所有字节
byte[] bytes = Files.readAllBytes(Paths.get("test.txt"));

// 字符流读取(BufferedReader)
try (BufferedReader br = Files.newBufferedReader(path, StandardCharsets.UTF_8)) {
    String line;
    while ((line = br.readLine()) != null) {}
}

2)写入文件
// 写入字符串列表
List<String> content = Arrays.asList("第一行", "第二行");
Files.write(Paths.get("test.txt"), content, StandardCharsets.UTF_8);

// 写入字节
Files.write(Paths.get("test.txt"), "Hello NIO.2".getBytes());

// 字符流写入
try (BufferedWriter bw = Files.newBufferedWriter(path)) {
    bw.write("Hello World");
}

如下实例:

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

public class ReadWriteOperations {
    public static void readFiles() throws Exception {
        Path path = Paths.get("test.txt");
        
        // 1. 读取所有字节
        byte[] bytes = Files.readAllBytes(path);
        
        // 2. 读取所有行
        List<String> lines = Files.readAllLines(path, StandardCharsets.UTF_8);
        
        // 3. 使用BufferedReader逐行读取
        try (BufferedReader reader = Files.newBufferedReader(path, StandardCharsets.UTF_8)) {
            String line;
            while ((line = reader.readLine()) != null) {
                System.out.println(line);
            }
        }
        
        // 4. 使用Stream API读取
        try (Stream<String> stream = Files.lines(path, StandardCharsets.UTF_8)) {
            stream.forEach(System.out::println);
        }
    }
    
    public static void writeFiles() throws Exception {
        Path path = Paths.get("output.txt");
        
        // 1. 写入字节
        String content = "Hello, NIO.2!";
        Files.write(path, content.getBytes(StandardCharsets.UTF_8));
        
        // 2. 写入字符串列表
        List<String> lines = List.of("Line 1", "Line 2", "Line 3");
        Files.write(path, lines, StandardCharsets.UTF_8);
        
        // 3. 使用BufferedWriter写入
        try (BufferedWriter writer = Files.newBufferedWriter(path, StandardCharsets.UTF_8)) {
            writer.write("Hello");
            writer.newLine();
            writer.write("World");
        }
        
        // 4. 追加写入
        Files.write(path, List.of("New line"), 
            StandardCharsets.UTF_8, StandardOpenOption.APPEND);
    }
}

四、文件属性操作

4.1 基本文件属性

1. 基础属性
BasicFileAttributes attrs = Files.readAttributes(path, BasicFileAttributes.class);
attrs.creationTime();    // 创建时间
attrs.lastAccessTime();  // 最后访问时间
attrs.lastModifiedTime();// 最后修改时间
attrs.size();            // 文件大小
attrs.isDirectory();     // 是否目录

2. DOS/Windows 属性
DosFileAttributes dosAttrs = Files.readAttributes(path, DosFileAttributes.class);
dosAttrs.isReadOnly();
dosAttrs.isHidden();

3. POSIX/Linux 属性(权限、所有者)
PosixFileAttributes posixAttrs = Files.readAttributes(path, PosixFileAttributes.class);
UserPrincipal owner = posixAttrs.owner(); // 文件所有者
Set<PosixFilePermission> permissions = posixAttrs.permissions(); // 权限

// 设置 Linux 权限(rwxr-x---)
Set<PosixFilePermission> perms = PosixFilePermissions.fromString("rwxr-x---");
Files.setPosixFilePermissions(path, perms);

4. 文件所有者
UserPrincipalLookupService lookup = FileSystems.getDefault().getUserPrincipalLookupService();
UserPrincipal owner = lookup.lookupPrincipalByName("root");
Files.setOwner(path, owner);

如下实例:

import java.nio.file.attribute.*;
import java.nio.file.*;
import java.util.Set;

public class FileAttributes {
    public static void basicAttributes() throws Exception {
        Path path = Paths.get("test.txt");
        
        // 1. BasicFileAttributes(所有文件系统都支持)
        BasicFileAttributes basicAttrs = Files.readAttributes(
            path, BasicFileAttributes.class);
        
        System.out.println("创建时间: " + basicAttrs.creationTime());
        System.out.println("最后访问时间: " + basicAttrs.lastAccessTime());
        System.out.println("最后修改时间: " + basicAttrs.lastModifiedTime());
        System.out.println("是否是目录: " + basicAttrs.isDirectory());
        System.out.println("是否是普通文件: " + basicAttrs.isRegularFile());
        System.out.println("是否是符号链接: " + basicAttrs.isSymbolicLink());
        System.out.println("文件大小: " + basicAttrs.size());
        
        // 2. 设置最后修改时间
        FileTime time = FileTime.fromMillis(System.currentTimeMillis());
        Files.setLastModifiedTime(path, time);
        
        // 3. 获取文件所有者
        FileOwnerAttributeView ownerView = Files.getFileAttributeView(
            path, FileOwnerAttributeView.class);
        UserPrincipal owner = ownerView.getOwner();
        System.out.println("文件所有者: " + owner.getName());
    }
    
    public static void posixAttributes() throws Exception {
        // POSIX文件系统属性(Linux/Unix)
        Path path = Paths.get("test.txt");
        PosixFileAttributes posixAttrs = Files.readAttributes(
            path, PosixFileAttributes.class);
        
        System.out.println("文件组: " + posixAttrs.group().getName());
        System.out.println("权限: " + posixAttrs.permissions());
        
        // 设置POSIX权限
        Set<PosixFilePermission> perms = PosixFilePermissions.fromString("rwxr-xr-x");
        Files.setPosixFilePermissions(path, perms);
    }
    
    public static void dosAttributes() throws Exception {
        // DOS/Windows文件系统属性
        Path path = Paths.get("test.txt");
        DosFileAttributes dosAttrs = Files.readAttributes(
            path, DosFileAttributes.class);
        
        System.out.println("只读: " + dosAttrs.isReadOnly());
        System.out.println("隐藏: " + dosAttrs.isHidden());
        System.out.println("系统文件: " + dosAttrs.isSystem());
        System.out.println("归档: " + dosAttrs.isArchive());
        
        // 设置DOS属性
        Files.setAttribute(path, "dos:readonly", true);
        Files.setAttribute(path, "dos:hidden", true);
    }
    
    public static void userDefinedAttributes() throws Exception {
        // 用户自定义属性
        Path path = Paths.get("test.txt");
        
        // 写入自定义属性
        UserDefinedFileAttributeView userView = Files.getFileAttributeView(
            path, UserDefinedFileAttributeView.class);
        
        String attrName = "user.mimetype";
        String attrValue = "text/plain";
        
        // 写入属性
        userView.write(attrName, 
            StandardCharsets.UTF_8.encode(attrValue));
        
        // 读取属性
        ByteBuffer buf = ByteBuffer.allocate(userView.size(attrName));
        userView.read(attrName, buf);
        buf.flip();
        String value = StandardCharsets.UTF_8.decode(buf).toString();
    }
}

小小知识总结:

1. Files.readAttributes(...) 核心方法
Files.readAttributes(路径, 要读取哪种属性.class);
含义:读取这个文件的各种属性信息

2. BasicFileAttributes.class 这个参数是什么意思?
表示我要读取最基础的文件属性(所有系统 Windows/Linux/Mac 都通用)
BasicFileAttributes 可以获取文件最核心的 5 个属性:
1)basicAttrs.size() 文件大小(字节)
2)basicAttrs.lastModifiedTime() 最后修改时间
3)basicAttrs.creationTime() 创建时间
4)basicAttrs.isRegularFile()	是否是普通文件
5)basicAttrs.isDirectory()	是否是文件夹

3. System.currentTimeMillis() 获取当前系统时间,返回的是一个毫秒数字(时间戳)。

4. FileTime.fromMillis(毫秒数)
Java NIO 规定:设置文件时间必须用 FileTime 类型,不能直接用数字,
所以这行是把毫秒转成 FileTime 对象。

5. Files.getFileAttributeView(...)
获取一个属性视图,你要什么属性,就拿什么视图。

6. FileOwnerAttributeView.class 我要的是 “文件所有者” 相关的视图
(只能看 / 改文件是谁的)

五、目录操作

5.1 目录遍历

NIO.2 支持递归遍历目录树,通过 FileVisitor 接口实现。

核心接口

  • preVisitDirectory:访问目录前

  • visitFile:访问文件

  • visitFileFailed:访问失败

  • postVisitDirectory:访问目录后

import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.io.IOException;
import java.util.stream.Stream;

public class DirectoryOperations {
    public static void listDirectory() throws Exception {
        Path dir = Paths.get(".");
        
        // 1. 简单列出目录内容
        try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir)) {
            for (Path entry : stream) {
                System.out.println(entry.getFileName());
            }
        }
        
        // 2. 使用glob模式过滤
        try (DirectoryStream<Path> stream = 
                Files.newDirectoryStream(dir, "*.{java,class}")) {
            for (Path entry : stream) {
                System.out.println(entry);
            }
        }
        
        // 3. 使用过滤器
        DirectoryStream.Filter<Path> filter = new DirectoryStream.Filter<Path>() {
            public boolean accept(Path entry) throws IOException {
                return Files.size(entry) > 1024;  // 大于1KB的文件
            }
        };
        
        try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir, filter)) {
            for (Path entry : stream) {
                System.out.println(entry);
            }
        }
    }
    
    public static void walkDirectory() throws Exception {
        Path start = Paths.get(".");
        
        // 1. 使用Files.walk(深度优先遍历)
        try (Stream<Path> stream = Files.walk(start)) {
            stream.filter(Files::isRegularFile)
                  .forEach(System.out::println);
        }
        
        // 2. 限制遍历深度
        try (Stream<Path> stream = Files.walk(start, 3)) {  // 最多3层深度
            stream.forEach(System.out::println);
        }
        
        // 3. 使用Files.find(可带过滤条件)
        try (Stream<Path> stream = Files.find(start, 10, 
                (path, attrs) -> attrs.isRegularFile() && 
                attrs.size() > 1024 * 1024)) {  // 大于1MB的文件
            stream.forEach(System.out::println);
        }
        
        // 4. 使用Files.list(只列出当前目录)
        try (Stream<Path> stream = Files.list(start)) {
            stream.forEach(System.out::println);
        }
    }
    
    public static void fileVisitorExample() throws Exception {
        Path start = Paths.get(".");
        
        Files.walkFileTree(start, new SimpleFileVisitor<Path>() {
            @Override
            public FileVisitResult preVisitDirectory(Path dir, 
                    BasicFileAttributes attrs) throws IOException {
                System.out.println("进入目录: " + dir);
                return FileVisitResult.CONTINUE;
            }
            
            @Override
            public FileVisitResult visitFile(Path file, 
                    BasicFileAttributes attrs) throws IOException {
                System.out.println("访问文件: " + file);
                return FileVisitResult.CONTINUE;
            }
            
            @Override
            public FileVisitResult visitFileFailed(Path file, 
                    IOException exc) throws IOException {
                System.err.println("访问失败: " + file);
                return FileVisitResult.CONTINUE;
            }
            
            @Override
            public FileVisitResult postVisitDirectory(Path dir, 
                    IOException exc) throws IOException {
                System.out.println("离开目录: " + dir);
                return FileVisitResult.CONTINUE;
            }
        });
    }
    
    public static void deleteDirectory() throws Exception {
        Path dir = Paths.get("toDelete");
        
        // 删除整个目录(包括子目录和文件)
        Files.walkFileTree(dir, new SimpleFileVisitor<Path>() {
            @Override
            public FileVisitResult visitFile(Path file, 
                    BasicFileAttributes attrs) throws IOException {
                Files.delete(file);
                return FileVisitResult.CONTINUE;
            }
            
            @Override
            public FileVisitResult postVisitDirectory(Path dir, 
                    IOException exc) throws IOException {
                Files.delete(dir);
                return FileVisitResult.CONTINUE;
            }
        });
        
        // 或者使用更简单的方法(Java 8+)
        Files.walk(dir)
             .sorted(Comparator.reverseOrder())
             .map(Path::toFile)
             .forEach(File::delete);
    }
}

六、文件监控

NIO.2 内置文件变化监听(监控创建、删除、修改、重命名),无需第三方框架。
1. 监听步骤
 1)创建 WatchService
 2)注册监听目录 + 监听事件
 3)循环获取监听事件
import java.nio.file.*;
import static java.nio.file.StandardWatchEventKinds.*;
import java.io.IOException;
import java.util.concurrent.TimeUnit;

public class FileWatcher {
    
    public static void watchDirectory() throws Exception {
        // 1. 获取WatchService实例
        WatchService watchService = FileSystems.getDefault().newWatchService();
        
        // 2. 要监控的目录
        Path dir = Paths.get(".");
        
        // 3. 注册要监控的事件类型
        WatchKey key = dir.register(watchService, 
            ENTRY_CREATE,    // 创建
            ENTRY_DELETE,    // 删除
            ENTRY_MODIFY,    // 修改
            OVERFLOW);       // 溢出(事件丢失)
        
        System.out.println("开始监控目录: " + dir.toAbsolutePath());
        
        // 4. 事件处理循环
        while (true) {
            // 等待事件,可设置超时
            key = watchService.poll(10, TimeUnit.SECONDS);
            
            if (key == null) {
                System.out.println("等待超时,继续监控...");
                continue;
            }
            
            // 处理事件
            for (WatchEvent<?> event : key.pollEvents()) {
                WatchEvent.Kind<?> kind = event.kind();
                
                // 处理溢出事件
                if (kind == OVERFLOW) {
                    System.out.println("事件溢出,可能丢失了一些事件");
                    continue;
                }
                
                // 获取发生事件的文件名
                @SuppressWarnings("unchecked")
                WatchEvent<Path> ev = (WatchEvent<Path>) event;
                Path filename = ev.context();
                
                // 输出事件信息
                System.out.printf("事件类型: %s, 文件: %s%n", kind.name(), filename);
                
                // 根据不同事件类型处理
                if (kind == ENTRY_CREATE) {
                    System.out.println("文件创建: " + filename);
                } else if (kind == ENTRY_DELETE) {
                    System.out.println("文件删除: " + filename);
                } else if (kind == ENTRY_MODIFY) {
                    System.out.println("文件修改: " + filename);
                }
            }
            
            // 重置key,继续接收事件
            boolean valid = key.reset();
            if (!valid) {
                System.out.println("监控key失效");
                break;
            }
        }
        
        // 5. 关闭watchService
        watchService.close();
    }
    
    public static void watchDirectoryRecursive() throws Exception {
        // 如果要监控子目录,需要为每个子目录单独注册
        Path startDir = Paths.get(".");
        WatchService watchService = FileSystems.getDefault().newWatchService();
        
        // 注册所有子目录
        Files.walkFileTree(startDir, new SimpleFileVisitor<Path>() {
            @Override
            public FileVisitResult preVisitDirectory(Path dir, 
                    BasicFileAttributes attrs) throws IOException {
                dir.register(watchService, 
                    ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY);
                return FileVisitResult.CONTINUE;
            }
        });
        
        // 处理事件(同上)
    }
}

七、异步文件操作

7.1 java 异步I/O

1. NIO.2 提供真正的异步 I/O(非阻塞、回调式),分为:
异步文件 I/O:AsynchronousFileChannel
异步网络 I/O:AsynchronousSocketChannel、AsynchronousServerSocketChannel

2. 异步I/O两种模式
1)Future 模式:返回 Future,主动获取结果
2)回调模式:CompletionHandler,完成后自动回调(推荐)

7.2 异步文件读取(回调模式)

Path path = Paths.get("test.txt");
AsynchronousFileChannel channel = AsynchronousFileChannel.open(path, StandardOpenOption.READ);

ByteBuffer buffer = ByteBuffer.allocate(1024);

// 异步读取,回调通知
channel.read(buffer, 0, buffer, new CompletionHandler<Integer, ByteBuffer>() {
    // 读取成功
    @Override
    public void completed(Integer result, ByteBuffer attachment) {
        attachment.flip();
        byte[] bytes = new byte[attachment.limit()];
        attachment.get(bytes);
        System.out.println(new String(bytes));
        attachment.clear();
    }

    // 读取失败
    @Override
    public void failed(Throwable exc, ByteBuffer attachment) {
        exc.printStackTrace();
    }
});

7.3 异步网络编程(服务端)

// 异步服务端
AsynchronousServerSocketChannel server = AsynchronousServerSocketChannel.open()
        .bind(new InetSocketAddress(8888));

// 异步接受连接
server.accept(null, new CompletionHandler<AsynchronousSocketChannel, Object>() {
    @Override
    public void completed(AsynchronousSocketChannel client, Object attachment) {
        // 再次监听新连接
        server.accept(null, this);
        
        // 处理客户端数据
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        client.read(buffer, buffer, new CompletionHandler<Integer, ByteBuffer>() {
            @Override
            public void completed(Integer result, ByteBuffer attachment) {
                attachment.flip();
                System.out.println("客户端消息:" + new String(attachment.array(), 0, result));
            }

            @Override
            public void failed(Throwable exc, ByteBuffer attachment) {}
        });
    }

    @Override
    public void failed(Throwable exc, Object attachment) {}
});

7.4AsynchronousFileChannel 异步实例

import java.nio.channels.*;
import java.nio.ByteBuffer;
import java.util.concurrent.Future;
import java.util.concurrent.ExecutionException;
import java.nio.charset.StandardCharsets;

public class AsyncFileOperations {
    
    public static void asyncReadWithFuture() throws Exception {
        Path path = Paths.get("largefile.txt");
        
        // 1. 打开异步文件通道
        AsynchronousFileChannel fileChannel = AsynchronousFileChannel.open(
            path, StandardOpenOption.READ);
        
        // 2. 创建缓冲区
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        
        // 3. 异步读取
        Future<Integer> operation = fileChannel.read(buffer, 0);
        
        // 4. 可以做其他事情...
        System.out.println("正在读取文件,可以处理其他任务...");
        
        // 5. 等待读取完成
        while (!operation.isDone()) {
            // 可以检查进度或做其他事情
            Thread.sleep(100);
        }
        
        // 6. 获取结果
        int bytesRead = operation.get();
        buffer.flip();
        
        byte[] data = new byte[bytesRead];
        buffer.get(data);
        System.out.println("读取内容: " + new String(data, StandardCharsets.UTF_8));
        
        buffer.clear();
        fileChannel.close();
    }
    
    public static void asyncReadWithCompletionHandler() throws Exception {
        Path path = Paths.get("largefile.txt");
        AsynchronousFileChannel fileChannel = AsynchronousFileChannel.open(
            path, StandardOpenOption.READ);
        
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        
        // 使用CompletionHandler回调
        fileChannel.read(buffer, 0, buffer, new CompletionHandler<Integer, ByteBuffer>() {
            @Override
            public void completed(Integer result, ByteBuffer attachment) {
                System.out.println("读取完成,字节数: " + result);
                attachment.flip();
                
                byte[] data = new byte[attachment.limit()];
                attachment.get(data);
                System.out.println("内容: " + new String(data, StandardCharsets.UTF_8));
                
                try {
                    fileChannel.close();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
            
            @Override
            public void failed(Throwable exc, ByteBuffer attachment) {
                System.err.println("读取失败: " + exc.getMessage());
                try {
                    fileChannel.close();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        });
        
        // 主线程可以继续执行其他任务
        System.out.println("异步读取已启动,主线程继续执行...");
        Thread.sleep(2000); // 等待异步操作完成
    }
    
    public static void asyncWrite() throws Exception {
        Path path = Paths.get("output.txt");
        AsynchronousFileChannel fileChannel = AsynchronousFileChannel.open(
            path, StandardOpenOption.WRITE, StandardOpenOption.CREATE);
        
        String data = "Hello, Async NIO.2!";
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        buffer.put(data.getBytes(StandardCharsets.UTF_8));
        buffer.flip();
        
        // 异步写入
        Future<Integer> writeResult = fileChannel.write(buffer, 0);
        
        // 等待写入完成
        int bytesWritten = writeResult.get();
        System.out.println("写入完成,字节数: " + bytesWritten);
        
        fileChannel.close();
    }
}

八、NIO.2 vs 旧 IO / 早期 NIO

特性 旧 IO (java.io.File) 早期 NIO (java.nio) NIO.2 (java.nio.file)
路径抽象 File 类 Path(不完善) Path 接口(完善)
文件操作 方法少、功能弱 复杂 Files 工具类(极简)
异步 I/O 不支持 不支持 原生支持
文件树遍历 手动递归 复杂 FileVisitor
文件监听 WatchService
文件属性 极少 有限 完整支持(权限 / 链接)
异常处理 混乱 复杂 统一、清晰

九、NIO.2 最佳实践

1)优先使用 Path + Files,完全抛弃 java.io.File
2)读写小文件直接用 Files.readAllLines()/Files.write(),代码极简
3)遍历目录用 Files.walkFileTree(),无需手动递归
4)监控文件变化用 WatchService,无需轮询
5)高并发 I/O 用异步通道(AsynchronousXXXChannel)
6)操作文件权限 / 属性直接用 NIO.2 原生 API,跨平台更稳定
总结
1)NIO.2 核心:Path(路径)+ Files(操作)+ 异步 I/O + 文件系统增强
2)核心能力:文件树遍历、文件监听、完整文件属性、异步非阻塞 I/O
3)地位:Java 官方推荐 I/O 方案,现代 Java 开发必须掌握
4)优势:API 简洁、功能强大、性能优异、跨平台兼容好

十、 NIO 练习题总结100道

模块一:Path 路径操作(15 题)

10.1 基础题(1-10)

如何通过 Paths 创建一个 Path 对象?

如何将 java.io.File 转换为 Path?

如何将 Path 转换为 java.io.File?

获取 Path 的文件名用哪个方法?

获取 Path 的父路径用哪个方法?

获取路径的根目录(如 C:\ /)用哪个方法?

判断一个路径是否是绝对路径?

路径拼接:Path.resolve() 的用法

路径标准化:Path.normalize() 解决什么问题?

获取路径中第 2 级目录名称(getName (1))

中级题(11-15)

如何获取两个路径之间的相对路径?

Path 中 subpath() 方法的使用规则

判断两个路径是否指向同一个文件(Files.isSameFile())

路径分隔符如何跨平台获取?

如何构造一个不存在的路径对象?

10.2 模块二:Files 工具类(25 题)

基础(16-30)

如何判断文件 / 目录是否存在?

创建单级目录 vs 多级目录

创建空文件

删除文件(文件不存在会抛异常)

安全删除文件(不存在不抛异常)

文件复制

文件移动 / 重命名

复制时覆盖已存在文件用哪个参数?

原子移动文件用哪个选项?

获取文件大小(字节)

读取文件所有字节

读取文件所有行(List

写入字符串列表到文件

追加内容到文件

获取文件最后修改时间

中级(31-40)

如何获取 BufferedReader/BufferedWriter

如何创建临时文件

如何创建临时目录

判断是否是普通文件

判断是否是目录

判断是否是隐藏文件

遍历一个目录下的直接子文件(list)

过滤目录中的 .txt 文件

文件大小超过 1GB 不能用哪个方法?为什么?

如何高效复制大文件?

10.3 模块三:文件属性操作(15 题)

基础(41-48)

读取文件基础属性(BasicFileAttributes)

获取文件创建时间

获取文件最后访问时间

判断是否是符号链接

Windows 系统读取 DOS 属性(只读、隐藏)

Linux 读取 POSIX 属性

获取文件所有者

修改文件最后修改时间

中级(49-55)

Linux 如何设置文件权限 rwxr-x---?

如何修改文件所有者?

如何判断文件是否是符号链接?

如何读取符号链接指向的真实路径?

如何创建符号链接?

如何获取文件的 MIME 类型?

批量读取文件属性(提高性能)

10.4 模块四:文件树遍历 FileVisitor(10 题)

文件树遍历的核心接口是什么?

SimpleFileVisitor 作用?

遍历目录时 4 个回调方法分别是什么?

如何跳过某个子目录不遍历?

如何在遍历到文件时立即终止遍历?

遍历失败如何处理?

遍历目录树并统计所有文件数量

遍历并删除所有 .log 结尾的文件

遍历并复制整个目录结构

遍历并查找大于 100MB 的大文件

10.5 模块五:文件系统监听 WatchService(10 题)

WatchService 能监听哪 3 种事件?

如何创建 WatchService?

目录注册监听的代码写法

如何获取监听事件?

如何获取发生变化的文件名?

监听必须重置 key,为什么?

如何监听多级子目录变化?

监听服务如何优雅关闭?

实际工作:监控日志文件变化

实际工作:监控配置文件热更新

10.6 模块六:NIO.2 异步 I/O(10 题)

NIO.2 异步 I/O 两种模式?

异步文件通道类名?

Future 模式异步读取

CompletionHandler 回调模式读取

异步写文件

异步网络通道:AsynchronousServerSocketChannel

异步 accept 连接

异步读取客户端消息

异步 I/O 适合什么业务场景?

异步 vs 阻塞 IO 优势?

10.7 模块七:企业实战场景(10 题)

批量备份当天生成的文件

文件去重(根据大小 + 修改时间)

目录定时清理(7 天前文件删除)

配置文件热加载

日志文件实时采集上传

文件上传后自动解压

递归统计目录占用空间

跨平台路径兼容处理

安全遍历(权限不足跳过)

文件完整性校验(读取并计算 MD5)

10.8 模块八:面试 / 高级综合题(5 题)

NIO.2 相比旧 IO 优势?

Path 和 File 区别?

Files 方法为什么都是静态?

WatchService 原理?

异步 I/O 在高并发文件服务中的应用

posted @ 2026-05-19 10:27  爱的万龙决  阅读(16)  评论(0)    收藏  举报