关注我的公众号:【编程朝花夕拾】,可获取首发内容。

01 引言
在Java
的文件处理工具中,RandomAccessFile
是一个独特而强大的存在。与其他顺序读写流不同,它允许我们在文件中任意位置进行读写操作,就像操作数组一样自由,程序可以直接跳转到文件的任意地方来读写数据.
这种特性使其在特定场景下成为不可替代的解决方案。
02 基本概念
RandomAccessFile
主要完成随机读取的功能,可以读取指定位置的内容。不属于InputStream
和OutputStream
类系的。继承关系如下:

2.1 说明
RandomAccessFile
是一个完全独立的类,完全脱离了InputStream
和OutputStream
,取而代之的事DataInput
和DataOutput
。支持读取和写入随机访问文件。随机访问文件的行为类似于存储在文件系统中的大字节数组。有一种称为文件指针的游标或索引到隐含数组中; 输入操作从文件指针开始读取字节,并将文件指针推进到读取的字节之后。
如果随机访问文件是在读/ 写模式下创建的,则输出操作也可用; 输出操作从文件指针开始写入字节,并使文件指针前进到写入的字节之后。写入超过隐含数组的当前端的输出操作会导致数组被扩展。文件指针可由getFilePointer
方法读取并由seek
方法设置。
通常,对于此类中的所有读取例程,如果在读取所需的字节数之前达到文件结束,则会引发EOFException
(一种IOException
)。如果由于文件结束以外的任何原因无法读取任何字节,则会引发除EOFException
以外的IOException
。特别是,如果流已关闭,则可能引发IOException
。
2.2 基本方法
构造方法
arduino
// 文件路劲和打开方式
public RandomAccessFile(String name, String mode)
// 文件和打开方式
public RandomAccessFile(File file, String mode)
mode 模式选择
"r"
:只读模式(不可写),其他模式会抛出异常"rw"
:读写模式(文件不存在会自动创建)"rws"
:同步读写模式,实时写入内容+元数据,并会自动持久化"rwd"
:同步读写模式,实时写入内容(元数据延迟写入),只有再调用close()
或getFD().getsync()
才会持久化
03 使用场景
-
大文件局部修改:
当需要修改大型文件中间某部分内容时(如修改二进制文件特定偏移量数据),无需重写整个文件。
-
日志追加写入:
在日志系统中高效追加内容,避免每次打开关闭文件。
-
多线程断点续传:
实现下载工具时,不同线程可同时写入文件的不同位置。
-
内存映射
更加快速的处理大文件
04 实战代码案例
4.1 修改文件指定位置的内容
我是一名程序员
我们需要将内容改成:"我是一名没有头发的程序员"
因为RandomAccessFile
的数据写属于覆盖写,所以我们只要找到"我是一名"的位置即可,一个汉字占3个字节,所以位置应该是第3*4=12
个字节的地方。
java
@Test
void test01() throws Exception {
try (RandomAccessFile raf = new RandomAccessFile("src/main/file/data.txt", "rw")) {
System.out.println(raf.length());
raf.seek(12);
raf.write("没有头发的程序员".getBytes(StandardCharsets.UTF_8));
}
}

4.2 高效的日志追加
日志的追加就很简单了,直接获取所有内容的字节数,并定位到该位置,直接写就可以了。
java
@Test
void test02() throws Exception {
try (RandomAccessFile raf = new RandomAccessFile("src/main/file/data.txt", "rw")) {
raf.seek(raf.length());
String log = "\r\n" + DateUtils.format(new Date()) + " 新日志追加";
raf.write(log.getBytes(StandardCharsets.UTF_8));
}
}

日志追加后指针会自动后移,也就是只要打开一次文件,后面可以一直调用write()
方法写,避免文件的重复打开。
4.3 断点续传
断点续传我们之前讨论过,我们用RandomAccessFile
精确的控制断点续传。
ini
@Test
void test03() throws Exception {
try (FileInputStream inputStream = new FileInputStream(Path.of("src/main/file/upload/10最大图片.jpg").toFile());
RandomAccessFile raf = new RandomAccessFile("src/main/file/temp/part.jpg", "rw")) {
byte[] bytes = new byte[1024];
int len;
int count = 0;
while ((len = inputStream.read(bytes)) != -1) {
raf.write(bytes, 0, len);
count++;
if (count >= 2) {
// 写入2M后抛出异常模拟服务器异常
throw new RuntimeException("系统崩溃了");
}
}
}
}
代码内容是将src/main/file/upload/10最大图片.jpg
以每次1M
的缓存写入到src/main/file/temp/part.jpg
, 并模拟系统异常,刚好传到2M
的地方崩溃。

我们可以看到此时文件已经生成,并且文件无法打开。
我们继续模拟重新上传,看看最终的文件完整不完整:
ini
@Test
void test04() throws Exception {
try (RandomAccessFile sraf = new RandomAccessFile("src/main/file/upload/10最大图片.jpg", "r");
RandomAccessFile raf = new RandomAccessFile("src/main/file/temp/part.jpg", "rw")) {
long length = raf.length();
System.out.println("已经上传的文件大小:" + length);
sraf.seek(length);
raf.seek(length);
byte[] bytes = new byte[1024];
int len;
while ((len = sraf.read(bytes)) != -1) {
raf.write(bytes, 0, len);
}
System.out.println("文件上传完毕......");
}
}

从结果可以看出:文件写入的起始位置为2048的字节处,最终成功展示完整图片。
4.4 内存映射
RandomAccessFile
可以通过MappedByteBuffer
通过内存映射读取文件,速度更快。Kafka
就使用了内存映射。我们来对比一下普通的流读写10M的数据和内存映射的读写速度对比。
ini
// 普通的读写
StopWatch watch = new StopWatch();
watch.start();
try (FileInputStream fileInputStream = new FileInputStream(Path.of("src/main/file/upload/10最大图片.jpg").toFile());
FileOutputStream outputStream = new FileOutputStream(Path.of("src/main/file/temp/part.jpg").toFile())) {
byte[] buffer = new byte[1024];
int len = 0;
while ((len = fileInputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, len);
}
}
watch.stop();
System.out.println(watch.getTotalTimeMillis());
// 内存映射
StopWatch watch2 = new StopWatch();
watch2.start();
try (RandomAccessFile sraf = new RandomAccessFile("src/main/file/upload/10最大图片.jpg", "r");
RandomAccessFile raf = new RandomAccessFile("src/main/file/temp/part1.jpg", "rw")) {
long length = sraf.length();
System.out.println("length:" + length);
MappedByteBuffer sBuffer = sraf.getChannel().map(MapMode.READ_ONLY, 0, length);
MappedByteBuffer mBuffer = raf.getChannel().map(MapMode.READ_WRITE, 0, length);
for (int i = 0; i < length; i += 1024) {
byte b = sBuffer.get(i);
mBuffer.put(i, b);
}
}
watch2.stop();
System.out.println(watch2.getTotalTimeMillis());

从运行结果来看,整整快了4倍。
05 小结
RandomAccessFile
为Java开发者提供了底层文件控制能力,特别适合需要随机定位修改的场景。当与内存映射技术结合时,能够实现超大规模文件的高效处理,性能提升可达4倍以上。