1. 概述

Java NIO 提供了 FileChannel 类,用于高效地进行文件的读写操作。相比传统的 I/O 流操作,FileChannel 提供了更底层、更灵活的控制能力,适用于需要高性能处理大文件的场景。

本文将介绍如何使用 FileChannelByteBuffer 进行文件读写,以及 FileChannel 的一些高级特性,如文件锁、内存映射、强制刷盘等。


2. FileChannel 的优势

FileChannel 的主要优势包括:

✅ 支持从文件的指定位置读写数据
✅ 可以将文件的一部分直接映射到内存,提升读写效率
✅ 支持高效的文件间数据传输
✅ 支持对文件的某一部分加锁,防止并发访问冲突
✅ 可以强制将缓存中的数据写入磁盘,防止数据丢失


3. 使用 FileChannel 读取文件

3.1 示例:读取文件内容

假设文件 test_read.in 中包含内容:

Hello world

测试代码如下:

@Test
public void givenFile_whenReadWithFileChannelUsingRandomAccessFile_thenCorrect() 
  throws IOException {
    try (RandomAccessFile reader = new RandomAccessFile("src/test/resources/test_read.in", "r");
        FileChannel channel = reader.getChannel();
        ByteArrayOutputStream out = new ByteArrayOutputStream()) {

        int bufferSize = 1024;
        if (bufferSize > channel.size()) {
           bufferSize = (int) channel.size();
        }
        ByteBuffer buff = ByteBuffer.allocate(bufferSize);

        while (channel.read(buff) > 0) {
            out.write(buff.array(), 0, buff.position());
            buff.clear();
        }
        
        String fileContent = new String(out.toByteArray(), StandardCharsets.UTF_8);
        assertEquals("Hello world", fileContent);
    }
}

3.2 打开 FileChannel

可以通过以下方式打开 FileChannel

使用 RandomAccessFile 打开(只读):

RandomAccessFile reader = new RandomAccessFile(file, "r");
FileChannel channel = reader.getChannel();

模式 "r" 表示只读模式,关闭 RandomAccessFile 会同时关闭 FileChannel

使用 FileInputStream 打开:

FileInputStream fin = new FileInputStream(file);
FileChannel channel = fin.getChannel();

同样,关闭 FileInputStream 也会关闭对应的 FileChannel

3.3 读取数据

从当前位置读取:

ByteBuffer buff = ByteBuffer.allocate(1024);
int noOfBytesRead = channel.read(buff);
String fileContent = new String(buff.array(), StandardCharsets.UTF_8);

assertEquals("Hello world", fileContent);

从指定位置读取:

ByteBuffer buff = ByteBuffer.allocate(1024);
int noOfBytesRead = channel.read(buff, 5);
String fileContent = new String(buff.array(), StandardCharsets.UTF_8);
assertEquals("world", fileContent);

⚠️ 注意:读取时需指定合适的字符集(如 StandardCharsets.UTF_8),否则可能导致乱码。


4. 使用 FileChannel 写入文件

4.1 示例:写入文件内容

@Test
public void whenWriteWithFileChannelUsingRandomAccessFile_thenCorrect() 
  throws IOException {
    String file = "src/test/resources/test_write_using_filechannel.txt";
    try (RandomAccessFile writer = new RandomAccessFile(file, "rw");
        FileChannel channel = writer.getChannel()) {
        
        ByteBuffer buff = ByteBuffer.wrap("Hello world".getBytes(StandardCharsets.UTF_8));
        channel.write(buff);
        
        // 验证写入结果
        RandomAccessFile reader = new RandomAccessFile(file, "r");
        assertEquals("Hello world", reader.readLine());
        reader.close();
    }
}

4.2 打开 FileChannel

使用 RandomAccessFile(读写模式):

RandomAccessFile writer = new RandomAccessFile(file, "rw");
FileChannel channel = writer.getChannel();

"rw" 表示读写模式。

使用 FileOutputStream 打开:

FileOutputStream fout = new FileOutputStream(file);
FileChannel channel = fout.getChannel();

4.3 写入数据

写入字节序列:

ByteBuffer buff = ByteBuffer.wrap("Hello world".getBytes(StandardCharsets.UTF_8));
channel.write(buff);

从指定位置写入:

ByteBuffer buff = ByteBuffer.wrap("Hello world".getBytes(StandardCharsets.UTF_8));
channel.write(buff, 5);

5. 获取和设置当前读写位置

FileChannel 支持获取和设置当前读写位置:

long originalPosition = channel.position();
channel.position(5);
assertEquals(originalPosition + 5, channel.position());

6. 获取文件大小

使用 size() 方法可以获取文件大小(单位为字节):

@Test
public void whenGetFileSize_thenCorrect() 
  throws IOException {
    RandomAccessFile reader = new RandomAccessFile("src/test/resources/test_read.in", "r");
    FileChannel channel = reader.getChannel();

    assertEquals(11, channel.size());

    channel.close();
    reader.close();
}

7. 截断文件

使用 truncate() 方法可以截断文件:

@Test
public void whenTruncateFile_thenCorrect() 
  throws IOException {
    String input = "this is a test input";

    FileOutputStream fout = new FileOutputStream("src/test/resources/test_truncate.txt");
    FileChannel channel = fout.getChannel();

    ByteBuffer buff = ByteBuffer.wrap(input.getBytes());
    channel.write(buff);

    channel = channel.truncate(5);
    assertEquals(5, channel.size());

    fout.close();
    channel.close();
}

8. 强制刷盘

为了防止系统缓存导致的数据丢失,可使用 force() 方法强制将数据写入磁盘:

channel.force(true); // true 表示同时刷元数据

⚠️ 注意:此方法只在文件位于本地设备时有效。


9. 将文件部分映射到内存

使用 map() 方法可以将文件的某一部分映射到内存中:

@Test
public void givenFile_whenReadAFileSectionIntoMemoryWithFileChannel_thenCorrect() 
  throws IOException { 
    try (RandomAccessFile reader = new RandomAccessFile("src/test/resources/test_read.in", "r");
        FileChannel channel = reader.getChannel()) {

        MappedByteBuffer buff = channel.map(FileChannel.MapMode.READ_ONLY, 6, 5);

        if (buff.hasRemaining()) {
            byte[] data = new byte[buff.remaining()];
            buff.get(data);
            assertEquals("world", new String(data, StandardCharsets.UTF_8));    
        }
    }
}

支持的映射模式:

  • FileChannel.MapMode.READ_ONLY:只读模式
  • FileChannel.MapMode.READ_WRITE:读写模式
  • FileChannel.MapMode.PRIVATE:私有模式,修改不会影响原文件

10. 锁定文件部分

使用 tryLock() 方法可以锁定文件的某一部分,防止并发访问冲突:

@Test
public void givenFile_whenWriteAFileUsingLockAFileSectionWithFileChannel_thenCorrect() 
  throws IOException { 
    try (RandomAccessFile reader = new RandomAccessFile("src/test/resources/test_read.in", "rw");
        FileChannel channel = reader.getChannel()) {
        
        FileLock fileLock = channel.tryLock(6, 5, false);
        assertNotNull(fileLock);
        
        // do other operations...
    }
}
  • 参数说明:
    • 6:锁定起始位置
    • 5:锁定长度
    • false:表示排他锁(exclusive),若为 true 表示共享锁(shared)

⚠️ 注意:部分操作系统可能不支持共享锁,默认使用排他锁。


11. 关闭 FileChannel

使用完 FileChannel 后必须关闭,推荐使用 try-with-resources

try (FileChannel channel = ...) {
    // 使用 channel
}

也可以手动关闭:

channel.close();

12. 总结

通过本文,我们了解了 FileChannel 的基本用法和高级特性:

✅ 支持随机读写
✅ 支持内存映射,提升性能
✅ 支持并发锁机制
✅ 支持强制刷盘,防止数据丢失

这些特性使 FileChannel 成为处理大文件或对数据一致性要求较高的应用中的首选方案。

完整示例代码可参考 GitHub


原始标题:Guide to Java FileChannel