讲讲随机访问文件类 RandomAccessFile 的作用、特有方法以及常见应用场景
在Java的I/O体系中,RandomAccessFile 是一个非常独特且强大的类。它不属于 InputStream 或 OutputStream 继承体系,而是直接继承自 Object,并实现了 DataInput 和 DataOutput 接口。
下面我们从作用、特有方法、常见应用场景以及代码示例四个方面来详细介绍它。
一、 RandomAccessFile 的作用
传统的输入流(FileInputStream)只能从头到尾顺序读取,输出流(FileOutputStream)也只能顺序写入(或者追加到末尾)。如果想要修改文件中间的某几个字节,传统做法是必须把整个文件读入内存,修改后再重新写回,这在处理大文件时效率极低。
RandomAccessFile 的核心作用就是支持“随机访问”文件。
- 既能读又能写:它一个类就兼具了读和写的功能。
- 支持任意位置读写:它内部维护了一个文件指针(File Pointer)。你可以自由地将指针移动到文件的任意位置,然后从该位置开始读取或写入数据。
- 类似于虚拟数组:你可以把文件看作一个存储在磁盘上的巨大字节数组,通过指针(索引)直接访问任意元素。
构造方法与工作模式
创建 RandomAccessFile 时,必须指定“模式”(mode):
java
RandomAccessFile raf = new RandomAccessFile("file.txt", "rw");
常见模式有:
"r":以只读方式打开。调用任何 write 方法都会抛出IOException。"rw":读写模式。如果文件不存在,会尝试创建。"rws":读写模式,并要求对文件内容或元数据的每个更新都同步写入底层存储设备。"rwd":读写模式,并要求对文件内容的每个更新都同步写入底层存储设备(比rws性能略好)。
二、 特有方法(核心方法)
除了 DataInput 和 DataOutput 提供的各种读写基本类型的方法(如 readInt(), writeDouble(), writeUTF() 等)之外,RandomAccessFile 最核心的是与文件指针和长度控制相关的方法:
| 方法名 | 作用 |
|---|---|
long getFilePointer() |
返回文件指针的当前位置(距离文件开头的字节偏移量)。 |
void seek(long pos) |
最核心的方法。将文件指针定位到指定的绝对位置 pos。下一次读写将从该位置开始。 |
int skipBytes(int n) |
尝试跳过 n 个字节的输入(只能向前跳,不能向后跳)。 |
long length() |
返回此文件的长度(字节数)。 |
void setLength(long newLength) |
设置文件的长度。可以用来截断文件或预分配(扩展)文件空间。 |
三、 常见应用场景
因为 RandomAccessFile 具有“任意定位”和“断点读写”的特性,它在以下场景中被广泛使用:
1. 多线程下载 / 断点续传
这是最经典的应用。
- 原理:在下载一个大文件时,可以开启 5 个线程,每个线程负责下载文件的一个分片(如第一个线程下载 0-100MB,第二个下载 100-200MB...)。
- 实现:下载前,使用
setLength()在磁盘上创建一个和源文件一样大的空文件。然后每个线程创建自己的RandomAccessFile对象,调用seek(startPosition)定位到自己负责的区间,并发写入。 - 断点续传:如果下载中断,记录下当前已下载的进度(Pointer 位置)。下次启动时,直接
seek到该位置继续下载。
2. 数据库存储引擎与索引文件
数据库(如 MySQL 的 InnoDB,或者本地轻量级数据库)需要频繁修改数据文件中的某一行。
- 原理:如果使用普通流,修改一条记录就要重写整个数据库文件,代价太大。
- 实现:数据库通过索引(B+树等)计算出某条记录在磁盘文件中的具体偏移量(Offset),然后使用
RandomAccessFile.seek(offset)直接定位并修改那几十个字节。
3. 读写大文件的元数据(如 MP3, MP4, ZIP)
很多媒体文件或压缩文件的关键信息(元数据)是存放在文件末尾或特定位置的。
- 例如 ZIP 文件:它的核心目录记录(Central Directory)通常在文件的末尾。
- 实现:使用
RandomAccessFile,可以直接seek(length() - 1024)跳到文件末尾去读取目录信息,而不需要把几百兆的压缩包从头到尾读一遍。
4. 日志收集与尾部监听(类似于 tail -f)
需要实时读取不断增长的日志文件的最新内容。
- 实现:记录上一次读取的指针位置。定时任务触发时,先
seek到上次的位置,读取新追加的日志,然后更新指针。
四、 代码示例
下面的代码演示了如何使用 RandomAccessFile 写入数据、在指定位置修改数据、以及读取数据。
java
import java.io.IOException;
import java.io.RandomAccessFile;
public class RandomAccessFileDemo {
public static void main(String[] args) {
String filePath = "temp.dat";
try (RandomAccessFile raf = new RandomAccessFile(filePath, "rw")) {
// 1. 写入一些数据
// 写入一个整型 (4字节) 和一个双精度浮点型 (8字节)
raf.writeInt(100);
raf.writeDouble(3.14159);
System.out.println("当前指针位置 (写入后): " + raf.getFilePointer()); // 应该输出 12 (4 + 8)
// 2. 移动指针到开头,读取数据
raf.seek(0); // 回到文件头
System.out.println("读取 Int: " + raf.readInt()); // 输出 100
System.out.println("读取 Double: " + raf.readDouble()); // 输出 3.14159
// 3. 修改中间的数据 (将 3.14159 改为 2.71828)
// Double 的数据从第 4 个字节开始
raf.seek(4);
raf.writeDouble(2.71828); // 覆盖原来的 3.14159
// 4. 再次读取验证
raf.seek(4);
System.out.println("修改后的 Double: " + raf.readDouble()); // 输出 2.71828
// 5. 在文件末尾追加数据
long fileLength = raf.length();
raf.seek(fileLength); // 定位到文件末尾
raf.writeUTF("Hello World");
// 6. 读取刚刚追加的内容
raf.seek(fileLength);
System.out.println("追加的内容: " + raf.readUTF());
} catch (IOException e) {
e.printStackTrace();
}
}
}
五、 缺点与注意事项
尽管 RandomAccessFile 功能强大,但它也有一些局限性:
- 只支持文件:它只能操作文件,不能操作网络流、内存流等。
- 性能问题:每次
seek都会涉及到磁盘磁头的寻道时间(在机械硬盘上较明显),频繁的随机读写性能低于顺序读写。通常需要结合缓冲区(Buffer)来提高性能。 - 不能直接插入数据:虽然它支持在任意位置修改(覆盖)数据,但不支持在中间插入数据。如果要在中间插入 10 个字节,必须手动将插入点之后的所有内容向后移动 10 个字节,否则后面的数据会被直接覆盖。