Skip to content

FileChannel

NOTE

FileChannel 是 Java NIO 中用于文件操作的通道,它提供了比传统 IO 流更高效的文件读写能力。需要注意的是,FileChannel 只能工作在阻塞模式下,不支持非阻塞操作。

获取 FileChannel

获取 FileChannel 有三种常见方式:

1. 通过 FileInputStream 获取(只读)

java
try (FileInputStream fis = new FileInputStream("data.txt");
     FileChannel channel = fis.getChannel()) {
    // 只能读取,不能写入
}

2. 通过 FileOutputStream 获取(只写)

java
try (FileOutputStream fos = new FileOutputStream("data.txt");
     FileChannel channel = fos.getChannel()) {
    // 只能写入,不能读取
}

3. 通过 RandomAccessFile 获取(可读可写)

java
try (RandomAccessFile raf = new RandomAccessFile("data.txt", "rw");
     FileChannel channel = raf.getChannel()) {
    // 可读可写,模式由 RandomAccessFile 的 mode 参数决定
    // "r" - 只读, "rw" - 读写
}

读取

使用 read() 方法从 FileChannel 读取数据到 ByteBuffer,返回值表示读到了多少字节,-1 表示到达了文件的末尾。

java
ByteBuffer buffer = ByteBuffer.allocate(1024);
int bytesRead = channel.read(buffer);

写入

使用 write() 方法将 ByteBuffer 中的数据写入 FileChannel:

java
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.put("Hello, FileChannel!".getBytes(StandardCharsets.UTF_8));
buffer.flip();  // 切换为读模式

// write() 返回写入的字节数
while (buffer.hasRemaining()) {
    channel.write(buffer);
}

WARNING

write() 方法不保证一次调用就能写入所有数据,因此需要在循环中调用,直到 buffer 中没有剩余数据。

关闭

FileChannel 使用完毕后必须关闭以释放系统资源。推荐使用 try-with-resources 语法:

java
try (FileChannel channel = FileChannel.open(Paths.get("data.txt"), 
        StandardOpenOption.READ, StandardOpenOption.WRITE)) {
    // 使用 channel
}  // 自动关闭

位置

FileChannel 维护一个内部的 position(位置指针),表示当前读写的位置。

获取当前位置

java
long pos = channel.position();

设置当前位置

java
long newPos = 100;
channel.position(newPos);

IMPORTANT

设置 position 时需要注意:

  • 如果 position 设置到文件末尾,此时读取会返回 -1
  • 如果 position 超过文件末尾再写入,新内容和原文件末尾之间会产生空洞(0x00 字节)

大小

使用 size() 方法获取文件的大小(字节数):

java
long fileSize = channel.size();

强制刷盘

操作系统出于性能考虑,会将数据先缓存在内存中,不会立刻写入磁盘。调用 force() 方法可以强制将数据刷入磁盘:

java
// true 表示同时刷新文件内容和元数据(权限、修改时间等)
// false 表示只刷新文件内容
channel.force(true);

文件截断

使用 truncate() 方法可以将文件截断到指定大小:

java
// 将文件截断为 1024 字节,超出部分会被丢弃
channel.truncate(1024);

零拷贝传输

FileChannel 提供了两个高效的零拷贝方法:

transferTo - 从当前 Channel 传输到目标 Channel

java
// 效率高,底层利用操作系统的零拷贝机制
// 适合文件到网络的传输
try (FileChannel from = FileChannel.open(Paths.get("source.txt"), StandardOpenOption.READ);
     SocketChannel to = SocketChannel.open()) {
    
    long position = 0;
    long count = from.size();
    // 最多传输 2GB,超过需要多次调用
    from.transferTo(position, count, to);
}

transferFrom - 从源 Channel 传输到当前 Channel

java
try (FileChannel to = FileChannel.open(Paths.get("dest.txt"), 
        StandardOpenOption.WRITE, StandardOpenOption.CREATE);
     FileChannel from = FileChannel.open(Paths.get("source.txt"), StandardOpenOption.READ)) {
    
    to.transferFrom(from, 0, from.size());
}

TIP

transferTo/transferFrom 利用 DMA(直接内存访问),数据直接在内核空间传输,避免了用户态和内核态之间的数据拷贝,效率远高于传统的 read/write 循环。

Path 与 Paths

java.nio.file.Path 是 JDK 7 引入的接口,用于表示文件系统中的路径。Paths 是用于创建 Path 实例的工具类。

创建 Path

java
// 使用 Paths.get()
Path path1 = Paths.get("data.txt");
Path path2 = Paths.get("/Users", "hyj", "data.txt");

// JDK 11+ 推荐使用 Path.of()
Path path3 = Path.of("data.txt");
Path path4 = Path.of("/Users", "hyj", "data.txt");

// 从 URI 创建
Path path5 = Paths.get(URI.create("file:///Users/hyj/data.txt"));

其中Paths.get("data.txt")是相对路径, 使用 user.dir 环境变量来定位,表示当前工作目录。

  • 如果你在 IDE(如 IntelliJ IDEA 或 Eclipse)中运行: 默认的工作目录通常是你的项目根目录(Project Root)。
  • 如果你在命令行运行: 工作目录就是你输入运行命令时所在的那个文件夹。

你可以通过这行代码打印出当前程序实际运行的基准路径:System.out.println(System.getProperty("user.dir"));

路径解析

  1. resolve (路径拼接)

逻辑: path1.resolve(path2)。如果 path2 是相对路径,它会接在 path1 后面;如果 path2 是绝对路径,则直接返回 path2(因为绝对路径已经到头了,没法再拼)。

  1. resolveSibling (替换最后一级) 逻辑: 它是先找到当前路径的 getParent()(父目录),然后再执行 resolve。它常用于在同一个目录下更改文件名。

  2. relativize (计算相对路径) 逻辑: 计算从 path1 出发,经过怎样的移动(如 .. 回退或进入子目录)能到达 path2。

限制:两个路径必须是同一类型的(要么都是相对路径,要么都是绝对路径),否则会抛出异常。

WARNING

这些方法都不会检查文件是否存在。它们只是单纯的字符串逻辑计算。即便你 resolve 出来的路径在硬盘上根本不存在,Java 也不会报错,直到你真正尝试用 Files.read() 等方法去读取它。

java
Path base = Paths.get("/Users/hyj");

// resolve - 拼接路径
Path resolved = base.resolve("Documents/data.txt");
// 结果: /Users/hyj/Documents/data.txt

// resolveSibling - 替换最后一级
Path sibling = Paths.get("/Users/hyj/old.txt").resolveSibling("new.txt");
// 结果: /Users/hyj/new.txt

// relativize - 计算相对路径
Path from = Paths.get("/Users/hyj");
Path to = Paths.get("/Users/hyj/Documents/data.txt");
Path relative = from.relativize(to);
// 结果: Documents/data.txt

路径规范化

java
// normalize - 消除 . 和 ..
Path messy = Paths.get("/Users/hyj/../hyj/./Documents");
Path clean = messy.normalize();
// 结果: /Users/hyj/Documents

// toAbsolutePath - 转为绝对路径
Path absolute = Paths.get("data.txt").toAbsolutePath();

// toRealPath - 解析符号链接,返回真实路径(文件必须存在)
Path real = Paths.get("link.txt").toRealPath();

路径信息

java
Path path = Paths.get("/Users/hyj/Documents/data.txt");

path.getFileName();     // data.txt
path.getParent();       // /Users/hyj/Documents
path.getRoot();         // / 
path.getNameCount();    // 4 (Users, hyj, Documents, data.txt)
path.getName(0);        // Users
path.getName(3);        // data.txt
path.isAbsolute();      // true

// 迭代路径组件
for (Path component : path) {
    System.out.println(component);
}
// 输出: Users, hyj, Documents, data.txt

其中getRoot()方法,对于UNIX / Linux / macOS,整个系统只有一个根,getRoot() 总是返回:/。对于Windows,每个分区(盘符)都是一个独立的根,如果路径是 C:\Users\data.txt,getRoot() 返回:C:\;如果路径是网络共享路径(UNC),如 \\Server\Share\file.txt,getRoot() 返回:\\Server\Share\

路径比较

java
Path p1 = Paths.get("/Users/hyj/data.txt");
Path p2 = Paths.get("/Users/hyj/data.txt");
Path p3 = Paths.get("/Users/hyj/other.txt");

p1.equals(p2);          // true
p1.compareTo(p3);       // 字典序比较

p1.startsWith("/Users");           // true
p1.endsWith("data.txt");           // true

Files 工具类

java.nio.file.Files 是 JDK 7 引入的文件操作工具类,提供了大量静态方法简化文件操作。

基本操作

java
Path path = Paths.get("data.txt");

// 判断文件是否存在
boolean exists = Files.exists(path);

// 创建文件
Files.createFile(path);

// 创建目录(父目录必须存在)
Files.createDirectory(Paths.get("mydir"));

// 创建多级目录
Files.createDirectories(Paths.get("parent/child/grandchild"));

// 删除文件或空目录
Files.delete(path);

// 删除(如果存在)
Files.deleteIfExists(path);

// 复制文件
Files.copy(Paths.get("source.txt"), Paths.get("dest.txt"), 
    StandardCopyOption.REPLACE_EXISTING);

// 移动/重命名文件
Files.move(Paths.get("old.txt"), Paths.get("new.txt"),
    StandardCopyOption.ATOMIC_MOVE);

读写文件

java
Path path = Paths.get("data.txt");

// 读取所有字节
byte[] bytes = Files.readAllBytes(path);

// 读取所有行
List<String> lines = Files.readAllLines(path, StandardCharsets.UTF_8);

// 写入字节
Files.write(path, "Hello".getBytes(StandardCharsets.UTF_8));

// 写入所有行
Files.write(path, List.of("line1", "line2"), StandardCharsets.UTF_8);

// 追加写入
Files.write(path, "append".getBytes(), StandardOpenOption.APPEND);

遍历目录

1. Files.newDirectoryStream - 浅层扫描

只遍历当前目录下的直接子文件和子文件夹,不会进入子目录内部。非递归

java
// 遍历目录下的直接子项
try (DirectoryStream<Path> stream = Files.newDirectoryStream(Paths.get("."))) {
    for (Path entry : stream) {
        System.out.println(entry.getFileName());
    }
}

支持 glob 模式过滤

java
// 只遍历 .java 文件
try (DirectoryStream<Path> stream = Files.newDirectoryStream(Paths.get("."), "*.java")) {
    for (Path entry : stream) {
        System.out.println(entry);
    }
}

// 匹配多种文件类型
try (DirectoryStream<Path> stream = Files.newDirectoryStream(Paths.get("."), "*.{java,xml,properties}")) {
    for (Path entry : stream) {
        System.out.println(entry);
    }
}

TIP

DirectoryStream 实现了 Closeable,务必使用 try-with-resources 或手动关闭,否则会导致文件句柄泄露。

2. Files.walkFileTree - 深度遍历(访问者模式)

递归遍历目录树,采用访问者模式,可以在进入/离开目录、访问文件等时机执行自定义逻辑。

java
Files.walkFileTree(Paths.get("."), new SimpleFileVisitor<>() {
    @Override
    public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) {
        System.out.println("进入目录: " + dir);
        return FileVisitResult.CONTINUE;
    }

    @Override
    public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
        System.out.println("访问文件: " + file);
        return FileVisitResult.CONTINUE;
    }

    @Override
    public FileVisitResult postVisitDirectory(Path dir, IOException exc) {
        System.out.println("离开目录: " + dir);
        return FileVisitResult.CONTINUE;
    }

    @Override
    public FileVisitResult visitFileFailed(Path file, IOException exc) {
        System.err.println("访问失败: " + file + " - " + exc.getMessage());
        return FileVisitResult.CONTINUE;  // 继续遍历,不因单个文件失败而中断
    }
});

FileVisitResult 枚举值

含义
CONTINUE继续遍历
TERMINATE立即终止整个遍历
SKIP_SUBTREE跳过当前目录的子树(仅在 preVisitDirectory 中有效)
SKIP_SIBLINGS跳过当前目录的后续兄弟节点

实战:递归删除目录

java
Path dirToDelete = Paths.get("temp");
Files.walkFileTree(dirToDelete, new SimpleFileVisitor<>() {
    @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;
    }
});

3. Files.walk - 深度遍历(Stream API)

JDK 8+ 引入,返回 Stream<Path>,更适合函数式编程风格。默认深度优先遍历。

java
// 遍历所有文件和目录
try (Stream<Path> paths = Files.walk(Paths.get("."))) {
    paths.forEach(System.out::println);
}

// 只处理普通文件
try (Stream<Path> paths = Files.walk(Paths.get("."))) {
    paths.filter(Files::isRegularFile)
         .forEach(System.out::println);
}

// 查找所有 .java 文件
try (Stream<Path> paths = Files.walk(Paths.get("src"))) {
    List<Path> javaFiles = paths
        .filter(p -> p.toString().endsWith(".java"))
        .toList();
}

// 限制遍历深度(第二个参数)
try (Stream<Path> paths = Files.walk(Paths.get("."), 2)) {
    paths.forEach(System.out::println);
}

IMPORTANT

Files.walk() 返回的 Stream 必须关闭。务必使用 try-with-resources,否则会导致资源泄露。

4. Files.find - 条件搜索

JDK 8+ 提供,比 walk + filter 更高效,因为过滤发生在遍历过程中。

java
// 查找 src 目录下所有大于 1KB 的 .java 文件,最大深度 10
try (Stream<Path> paths = Files.find(Paths.get("src"), 10,
        (path, attrs) -> attrs.isRegularFile() 
                         && path.toString().endsWith(".java")
                         && attrs.size() > 1024)) {
    paths.forEach(System.out::println);
}

三种遍历方式对比

方式递归风格适用场景
newDirectoryStream迭代器单层目录遍历,支持 glob 过滤
walkFileTree访问者模式需要精细控制(删除、复制目录树)
walkStream API函数式过滤和收集
findStream API按条件搜索,性能更优

文件属性

java
Path path = Paths.get("data.txt");

// 获取基本属性
BasicFileAttributes attrs = Files.readAttributes(path, BasicFileAttributes.class);
System.out.println("Size: " + attrs.size());
System.out.println("Created: " + attrs.creationTime());
System.out.println("Modified: " + attrs.lastModifiedTime());
System.out.println("Is Directory: " + attrs.isDirectory());

// 判断方法
Files.isReadable(path);
Files.isWritable(path);
Files.isExecutable(path);
Files.isHidden(path);
Files.isSymbolicLink(path);

总结

场景推荐方案
简单文件读写Files 工具类
大文件处理FileChannel + ByteBuffer
文件到网络传输FileChannel.transferTo()
需要随机访问FileChannel + position()
需要内存映射FileChannel.map()