基于本文回答

播面 播面

文图音视,全方位拆解八股文
0
评论

讲讲随机访问文件类 RandomAccessFile 的作用、特有方法以及常见应用场景

在Java的I/O体系中,RandomAccessFile 是一个非常独特且强大的类。它不属于 InputStreamOutputStream 继承体系,而是直接继承自 Object,并实现了 DataInputDataOutput 接口。

下面我们从作用特有方法常见应用场景以及代码示例四个方面来详细介绍它。


一、 RandomAccessFile 的作用

传统的输入流(FileInputStream)只能从头到尾顺序读取,输出流(FileOutputStream)也只能顺序写入(或者追加到末尾)。如果想要修改文件中间的某几个字节,传统做法是必须把整个文件读入内存,修改后再重新写回,这在处理大文件时效率极低。

RandomAccessFile 的核心作用就是支持“随机访问”文件

  1. 既能读又能写:它一个类就兼具了读和写的功能。
  2. 支持任意位置读写:它内部维护了一个文件指针(File Pointer)。你可以自由地将指针移动到文件的任意位置,然后从该位置开始读取或写入数据。
  3. 类似于虚拟数组:你可以把文件看作一个存储在磁盘上的巨大字节数组,通过指针(索引)直接访问任意元素。

构造方法与工作模式

创建 RandomAccessFile 时,必须指定“模式”(mode):

java
RandomAccessFile raf = new RandomAccessFile("file.txt", "rw");

常见模式有:

  • "r":以只读方式打开。调用任何 write 方法都会抛出 IOException
  • "rw":读写模式。如果文件不存在,会尝试创建。
  • "rws":读写模式,并要求对文件内容或元数据的每个更新都同步写入底层存储设备。
  • "rwd":读写模式,并要求对文件内容的每个更新都同步写入底层存储设备(比 rws 性能略好)。

二、 特有方法(核心方法)

除了 DataInputDataOutput 提供的各种读写基本类型的方法(如 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 功能强大,但它也有一些局限性:

  1. 只支持文件:它只能操作文件,不能操作网络流、内存流等。
  2. 性能问题:每次 seek 都会涉及到磁盘磁头的寻道时间(在机械硬盘上较明显),频繁的随机读写性能低于顺序读写。通常需要结合缓冲区(Buffer)来提高性能。
  3. 不能直接插入数据:虽然它支持在任意位置修改(覆盖)数据,但不支持在中间插入数据。如果要在中间插入 10 个字节,必须手动将插入点之后的所有内容向后移动 10 个字节,否则后面的数据会被直接覆盖。
00:00
00:00