1. 概述
Java NIO 提供了 FileChannel
类,用于高效地进行文件的读写操作。相比传统的 I/O 流操作,FileChannel
提供了更底层、更灵活的控制能力,适用于需要高性能处理大文件的场景。
本文将介绍如何使用 FileChannel
和 ByteBuffer
进行文件读写,以及 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。