随机访问文件
本教程共 100 篇 · 第 90 篇 · 更新于 2026-08-05 · 约 5 分钟阅读
90. 随机访问文件
本节目标:理解文件指针的概念,能用 RandomAccessFile 在文件任意位置精确读写。
顺序流的局限
到目前为止用过的所有流,都有一个共同特点:只能从头往后走,而且只能单向。
FileInputStream 只能读,FileOutputStream 只能写,读到第 100 个字节就回不去第 50 个了。想改文件中间的某个值,只能整个读出来、改完再整个写回去。
对小文件这么干无所谓。但如果是一个 2GB 的数据文件,只为改 4 个字节就全量重写一遍,显然不合理。
RandomAccessFile 解决的就是这个问题。
它不是流
先纠正一个认知:RandomAccessFile 不属于四大流体系。
它直接继承 Object,只实现了 DataInput 和 DataOutput 两个接口。所以它同时具备读和写的能力,方法名和上一节的数据流几乎一样:readInt()、writeDouble()、readUTF()。
可以把它理解成「一个能自由移动读写头的文件操作器」,更像是对操作系统文件 API 的直接映射。
文件指针
RandomAccessFile 的核心概念是文件指针,可以想成一个游标,标记当前读写的位置。
- 刚打开文件时,指针在 0,也就是文件开头
- 每读或写 n 个字节,指针自动往后移 n
- 调用
seek(long pos)可以把指针挪到任意位置
三个相关方法:
| 方法 | 作用 |
|---|---|
getFilePointer() | 返回当前指针位置 |
seek(long pos) | 把指针移动到指定字节偏移 |
length() | 返回文件总字节数 |
四种打开模式
构造方法的第二个参数是模式字符串,必须传:
| 模式 | 含义 |
|---|---|
"r" | 只读。文件不存在会抛异常 |
"rw" | 读写。文件不存在会自动创建 |
"rws" | 读写,且内容和元数据都同步刷盘 |
"rwd" | 读写,仅内容同步刷盘 |
日常用 "r" 和 "rw" 就够了。后两种会绕过操作系统缓存,每次写入都真正落盘,速度慢很多,只在数据绝对不能丢的场景才考虑。
第一个例子
import java.io.*;
public class RafBasic {
public static void main(String[] args) throws IOException {
try (RandomAccessFile raf = new RandomAccessFile("data.bin", "rw")) {
raf.writeInt(100); // 占 4 字节,位置 0-3
raf.writeInt(200); // 占 4 字节,位置 4-7
raf.writeInt(300); // 占 4 字节,位置 8-11
System.out.println("当前指针:" + raf.getFilePointer());
System.out.println("文件长度:" + raf.length());
raf.seek(4); // 跳到第二个整数
System.out.println("第二个数:" + raf.readInt());
raf.seek(4); // 再跳回去
raf.writeInt(999); // 原地覆盖
raf.seek(0);
System.out.print("全部内容:");
for (int i = 0; i < 3; i++) {
System.out.print(raf.readInt() + " ");
}
System.out.println();
}
}
}
```text
输出:
```text
当前指针:12
文件长度:12
第二个数:200
全部内容:100 999 300
注意 writeInt(999) 那一步:它没有插入新数据,而是把原位置的 4 个字节直接覆盖掉了。
Warning
RandomAccessFile的写入永远是覆盖,不是插入。想在中间插入内容,只能把后面的数据整体往后挪,或者重建文件。这是它和文本编辑器的最大区别。
定长记录的随机定位
随机访问真正的威力,体现在定长记录的文件上。
假设每条记录固定占 20 字节,那么第 n 条记录的起始位置就是 n * 20。要读第 500 条,直接 seek(500 * 20) 一步到位,不用遍历前面 499 条。
数据库的索引文件、游戏存档、日志的定位表,用的都是这个思路。
import java.io.*;
public class FixedRecord {
// 每条记录:id(4) + score(8) = 12 字节
private static final int RECORD_SIZE = 12;
public static void main(String[] args) throws IOException {
try (RandomAccessFile raf = new RandomAccessFile("scores.bin", "rw")) {
// 写入 10 条记录
for (int i = 0; i < 10; i++) {
raf.writeInt(i);
raf.writeDouble(60 + i * 3.5);
}
// 直接读第 7 条(下标 6),不遍历前面的
raf.seek(6L * RECORD_SIZE);
int id = raf.readInt();
double score = raf.readDouble();
System.out.printf("第 7 条:id=%d score=%.1f%n", id, score);
// 把第 7 条的分数改成 100
raf.seek(6L * RECORD_SIZE + 4); // 跳过 id 占的 4 字节
raf.writeDouble(100.0);
raf.seek(6L * RECORD_SIZE);
System.out.printf("修改后:id=%d score=%.1f%n",
raf.readInt(), raf.readDouble());
}
}
}
```text
输出:
```text
第 7 条:id=6 score=81.0
修改后:id=6 score=100.0
seek(6L * RECORD_SIZE) 里的 6L 别写成 6。seek 参数是 long,记录数一大,int 相乘会溢出。
Tip定长设计的代价是浪费空间。字符串字段必须按最大长度预留,短的补空白。所以它适合数值型数据,不适合长度差异巨大的文本。
追加与截断
想在文件末尾追加内容,先把指针挪到末尾:
raf.seek(raf.length()); // 指针移到文件尾
raf.writeInt(42); // 从这里开始写就是追加
```text
`setLength(long newLength)` 可以改变文件大小:
- 传入比当前小的值,多余部分被丢弃,相当于截断
- 传入比当前大的值,文件被扩展,新增部分内容未定义
```java
raf.setLength(0); // 清空文件
清空文件用这一句,比删了重建简洁得多。
现代替代方案
RandomAccessFile 是 Java 1.0 时代的产物,API 有些陈旧。
Java 7 引入的 NIO.2 提供了 SeekableByteChannel 接口,功能类似但设计更现代,也能和 ByteBuffer、Path 体系配合:
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.SeekableByteChannel;
import java.nio.file.*;
public class ChannelDemo {
public static void main(String[] args) throws IOException {
Path path = Path.of("channel.bin");
try (SeekableByteChannel channel = Files.newByteChannel(
path, StandardOpenOption.CREATE,
StandardOpenOption.READ, StandardOpenOption.WRITE)) {
ByteBuffer buffer = ByteBuffer.allocate(8);
buffer.putInt(11).putInt(22);
buffer.flip();
channel.write(buffer);
channel.position(4); // 相当于 seek(4)
buffer.clear();
buffer.limit(4);
channel.read(buffer);
buffer.flip();
System.out.println("读到:" + buffer.getInt());
}
}
}
```text
输出:
```text
读到:22
position() 对应 seek(),size() 对应 length()。
对初学者来说,RandomAccessFile 更直观,先掌握它没问题。等接触到高并发文件处理,再转向 Channel 体系。
什么时候真的需要随机访问
大多数日常开发用不上 RandomAccessFile,别为了用而用。
判断标准是:你是不是只关心文件的一小部分,而文件本身又很大。满足这两条,随机访问才有价值。
典型场景有这么几类。
一是断点续传。下载中断后重新开始,先看本地文件已有多少字节,seek 到那个位置继续写,前面下好的部分不用重来。
二是读取文件头。很多格式在开头几十个字节里放了标识信息。判断一张图是 PNG 还是 JPG,读前 8 个字节就够了,没必要加载整个文件。
三是原地更新计数器。比如一个统计文件在固定偏移量存着访问次数,每次读出来加一再写回原位,全程只碰 8 个字节。
反过来,如果你要处理的是整个文件的内容,或者文件本身就几十 KB,那老老实实用缓冲流或者 Files.readAllBytes(),代码简单得多。
Note频繁
seek会打乱操作系统的预读机制。顺序读一个文件往往比反复跳读同样多的数据还快,因为磁盘和缓存都是为顺序访问优化的。不要以为「随机访问」天然更高效。
并发访问要小心
RandomAccessFile 本身不是线程安全的。
多个线程共用同一个实例,一个线程刚 seek(100),另一个线程立刻 seek(200),第一个线程接着读到的就是错位的数据。文件指针是共享状态,谁改都生效。
解决办法有两个:要么每个线程各开一个 RandomAccessFile 实例,要么对读写操作整体加锁。前者更好,因为各自持有独立的文件指针,互不干扰。
本节小结
RandomAccessFile不属于流体系,它同时支持读和写。- 文件指针记录当前位置,
seek()可移动到任意字节偏移。 - 模式
"r"只读、"rw"读写并自动创建文件。 - 写入是覆盖不是插入,中间插数据必须挪动后续内容。
- 定长记录 +
seek(n * 记录长度)是随机定位的经典套路。 setLength(0)用来清空文件,seek(length())用来追加。- NIO.2 的
SeekableByteChannel是它的现代替代品。