首页 / Java 入门教程 / 随机访问文件

Java 入门教程

随机访问文件

本教程共 100 篇 · 第 90 篇 · 更新于 2026-08-05 · 约 5 分钟阅读

JavaJava 入门教程RandomAccessFile文件指针随机读写seek

90. 随机访问文件

本节目标:理解文件指针的概念,能用 RandomAccessFile 在文件任意位置精确读写。

顺序流的局限

到目前为止用过的所有流,都有一个共同特点:只能从头往后走,而且只能单向

FileInputStream 只能读,FileOutputStream 只能写,读到第 100 个字节就回不去第 50 个了。想改文件中间的某个值,只能整个读出来、改完再整个写回去。

对小文件这么干无所谓。但如果是一个 2GB 的数据文件,只为改 4 个字节就全量重写一遍,显然不合理。

RandomAccessFile 解决的就是这个问题。

它不是流

先纠正一个认知:RandomAccessFile 不属于四大流体系。

它直接继承 Object,只实现了 DataInputDataOutput 两个接口。所以它同时具备读和写的能力,方法名和上一节的数据流几乎一样: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 别写成 6seek 参数是 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 接口,功能类似但设计更现代,也能和 ByteBufferPath 体系配合:

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 是它的现代替代品。