在 Java 中,随机访问文件(Random Access Files)允许你 非顺序地 读写文件内容,也就是说,你可以跳到文件中的任意位置读取或写入数据,而不必从头到尾顺序操作。
这种功能通常用于:
- 大型文件处理(只读取部分内容)
- 日志文件更新(在文件中间写入新数据)
- 数据库文件或二进制文件操作
核心接口:SeekableByteChannel
SeekableByteChannel 是 NIO 提供的 可寻址通道,它扩展了 Channel I/O,并增加了当前位置概念。
常用方法:
| 方法 | 功能 |
|---|---|
position() |
获取通道当前的位置(偏移量) |
position(long) |
设置通道当前的位置(跳到文件某个偏移量) |
read(ByteBuffer) |
从当前通道位置读取数据到缓冲区 |
write(ByteBuffer) |
将缓冲区的数据写入当前通道位置 |
truncate(long) |
截断文件到指定长度 |
小贴士:通过 Path.newByteChannel() 或 Files.newByteChannel() 可以获得 SeekableByteChannel 实例。在默认文件系统下,也可以将其 强转为 FileChannel,这样可以使用更多高级功能,如文件映射、文件锁、绝对位置读写等。
示例讲解
假设我们有一个文件 file.txt,我们希望:
- 读取文件开头的 12 个字节
- 在开头写入
"I was here!" - 将文件开头的 12 个字节复制到文件末尾
- 再次写入
"I was here!"
import java.nio.file.*;
import java.nio.channels.FileChannel;
import java.nio.ByteBuffer;
import java.io.IOException;
import static java.nio.file.StandardOpenOption.*;
public class RandomAccessDemo {
public static void main(String[] args) {
Path file = Paths.get("file.txt");
String s = "I was here!\n";
byte[] data = s.getBytes();
ByteBuffer out = ByteBuffer.wrap(data); // 包装要写入的数据
ByteBuffer copy = ByteBuffer.allocate(12); // 用于存储文件开头的 12 字节
try (FileChannel fc = FileChannel.open(file, READ, WRITE)) {
// 1️⃣ 读取文件前 12 个字节
int nread;
do {
nread = fc.read(copy);
} while (nread != -1 && copy.hasRemaining());
// 2️⃣ 写入 "I was here!" 到文件开头
fc.position(0); // 跳到文件开头
while (out.hasRemaining()) {
fc.write(out);
}
out.rewind(); // 复位缓冲区,用于后续写入
// 3️⃣ 移动到文件末尾,并复制前 12 个字节到末尾
long length = fc.size();
fc.position(length); // 文件末尾
copy.flip(); // 切换为读模式
while (copy.hasRemaining()) {
fc.write(copy);
}
// 4️⃣ 如果需要再次写入 "I was here!" 到文件末尾,需要指定out.rewind();
while (out.hasRemaining()) {
fc.write(out);
}
System.out.println("文件操作完成!");
} catch (IOException e) {
System.err.println("I/O 异常: " + e.getMessage());
}
}
}
核心讲解点
随机访问的关键是 position()
- 可以用
fc.position(偏移量)定位到文件任意位置 - 后续的
read()或write()都从这个位置开始
缓冲区管理(ByteBuffer)
- 写入前用
ByteBuffer.wrap() - 多次写入需要
out.rewind()或flip()来复位缓冲区
FileChannel vs SeekableByteChannel
FileChannel功能更强大,可实现 文件映射、锁定、绝对位置读写SeekableByteChannel简单操作即可满足随机访问需求
异常处理
- 所有 I/O 操作都可能抛出
IOException - 使用 try-with-resources 自动关闭通道,保证资源释放
知识扩展
RandomAccessFile 是 Java 中一个功能强大但常被忽视的类,它允许你像访问一个大型字节数组一样随意跳转到文件的任意位置进行读写操作。与普通的顺序读写流(如 FileInputStream/FileOutputStream)不同,它支持随机访问——即可以在文件任意位置读取或写入数据,而无需从头开始。
基本读写操作
import java.io.RandomAccessFile;
import java.io.IOException;
public class RandomAccessFileDemo {
public static void main(String[] args) {
String filePath = "employee.dat";
try (RandomAccessFile raf = new RandomAccessFile(filePath, "rw")) {
// 定义记录结构:ID(4字节)+ 姓名(UTF-8,最大20字节)+ 年龄(4字节)+ 工资(8字节)
// 每条记录固定长度 = 4 + 20 + 4 + 8 = 36 字节(简化,实际UTF-8长度可变)
// 为简化,这里用固定长度的字符串(20个字符)
// 写入记录1
raf.seek(0);
raf.writeInt(1001);
raf.writeUTF("张三"); // 实际长度不定,此处简化
// 我们使用固定长度写入
raf.writeInt(28);
raf.writeDouble(5000.0);
// 写入记录2
raf.seek(36); // 跳到第二条记录
raf.writeInt(1002);
raf.writeUTF("李四");
raf.writeInt(35);
raf.writeDouble(6200.0);
// 读取记录2(从开头偏移36字节)
raf.seek(36);
int id = raf.readInt();
String name = raf.readUTF();
int age = raf.readInt();
double salary = raf.readDouble();
System.out.printf("ID: %d, 姓名: %s, 年龄: %d, 工资: %.2f%n", id, name, age, salary);
// 更新记录1的工资
raf.seek(0 + 4 + 20 + 4); // 跳过ID(4) + 姓名(20) + 年龄(4)
raf.writeDouble(5500.0);
// 再次读取记录1验证
raf.seek(0);
id = raf.readInt();
name = raf.readUTF();
age = raf.readInt();
salary = raf.readDouble();
System.out.printf("更新后: ID: %d, 姓名: %s, 年龄: %d, 工资: %.2f%n", id, name, age, salary);
} catch (IOException e) {
e.printStackTrace();
}
}
}
注意:上述示例中 writeUTF 写入的是变长字符串,实际长度不固定,因此计算偏移不准确。实际应用应使用固定长度字段或额外存储长度信息。
实现固定长度记录(推荐)
import java.io.RandomAccessFile;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
public class FixedLengthRecord {
private static final int ID_SIZE = 4;
private static final int NAME_SIZE = 20; // 字节数
private static final int AGE_SIZE = 4;
private static final int SALARY_SIZE = 8;
private static final int RECORD_SIZE = ID_SIZE + NAME_SIZE + AGE_SIZE + SALARY_SIZE;
// 写入记录
public static void writeRecord(RandomAccessFile raf, int id, String name, int age, double salary) throws IOException {
raf.writeInt(id);
byte[] nameBytes = name.getBytes(StandardCharsets.UTF_8);
byte[] nameBuf = new byte[NAME_SIZE];
System.arraycopy(nameBytes, 0, nameBuf, 0, Math.min(nameBytes.length, NAME_SIZE));
raf.write(nameBuf);
raf.writeInt(age);
raf.writeDouble(salary);
}
// 读取记录
public static void readRecord(RandomAccessFile raf, int recordNum) throws IOException {
raf.seek((long) recordNum * RECORD_SIZE);
int id = raf.readInt();
byte[] nameBuf = new byte[NAME_SIZE];
raf.readFully(nameBuf);
String name = new String(nameBuf, StandardCharsets.UTF_8).trim();
int age = raf.readInt();
double salary = raf.readDouble();
System.out.printf("ID: %d, 姓名: %s, 年龄: %d, 工资: %.2f%n", id, name, age, salary);
}
public static void main(String[] args) throws IOException {
try (RandomAccessFile raf = new RandomAccessFile("fixed.dat", "rw")) {
// 写入三条记录
writeRecord(raf, 1, "张三", 25, 5000);
writeRecord(raf, 2, "李四", 30, 6000);
writeRecord(raf, 3, "王五", 28, 5500);
// 读取第二条记录
readRecord(raf, 1); // 索引从0开始
// 更新第二条记录的工资
raf.seek(1 * RECORD_SIZE + ID_SIZE + NAME_SIZE + AGE_SIZE);
raf.writeDouble(6800.0);
System.out.println("更新后:");
readRecord(raf, 1);
}
}
}
断点续传模拟(多线程下载位置保存)
import java.io.RandomAccessFile;
import java.io.IOException;
public class DownloadStatus {
private static final String STATUS_FILE = "download.status";
// 保存已下载的字节数
public static void saveProgress(long bytes) throws IOException {
try (RandomAccessFile raf = new RandomAccessFile(STATUS_FILE, "rw")) {
raf.setLength(0); // 清空
raf.writeLong(bytes);
}
}
// 读取已下载的字节数
public static long loadProgress() throws IOException {
try (RandomAccessFile raf = new RandomAccessFile(STATUS_FILE, "r")) {
if (raf.length() < 8) {
return 0;
}
return raf.readLong();
}
}
public static void main(String[] args) throws IOException {
long progress = loadProgress();
System.out.println("上次下载进度: " + progress + " 字节");
// 模拟下载,每10秒更新一次进度
for (long i = progress; i < 1024 * 1024 * 10; i += 1024 * 10) {
// 模拟下载
saveProgress(i);
System.out.println("当前进度: " + i);
try { Thread.sleep(100); } catch (InterruptedException e) {}
}
System.out.println("下载完成!");
}
}













