前兩節介紹的 InputStream/OutputStream 和 Reader/Writer 都有兩大先天限制:
mark/reset 但極受限)。RandomAccessFile 打破了這兩大限制:可讀可寫、可隨機跳轉。它更接近操作系統文件 API(seek/read/write),是實現嵌入式數據庫、索引文件、斷點續傳等系統級組件的利器。
// 文件路徑或 File 對象 + 模式 public RandomAccessFile(String name, String mode) throws FileNotFoundException public RandomAccessFile(File file, String mode) throws FileNotFoundException
模式參數:
"r":只讀"rw":讀寫(最常用)"rws":讀寫 + 內容與元數據同步刷盤(最慢,最安全)"rwd":讀寫 + 內容同步刷盤,元數據不同步(折中)// 獲取當前指針位置 public native long getFilePointer() throws IOException; // 移動指針到指定位置 public native void seek(long pos) throws IOException; // 獲取文件長度 public native long length() throws IOException; // 截斷/擴展文件 public native void setLength(long newLength) throws IOException;
直接讀寫基本類型,無需手動拆裝箱:
readInt() / writeInt(int v) readLong() / writeLong(long v) readUTF() / writeUTF(String str) readDouble() / writeDouble(double v) readFully(byte[] b) // 保證讀滿,否則拋 EOFException
writeBytes(String s):假設 1 byte = 1 char,中文會亂碼。readLine():同理,無編碼概念,棄用。Map<String, byte[]>:put/get/remove/flush/closeseek 實現 O(1) 定位讀寫name.data:值數據文件,每條記錄固定 1024 字節(4 字節長度 + 1020 字節數據 + 填充)name.meta:元數據文件,存儲索引 Map 與空洞隊列,啟動時加載,關閉時持久化// 內存索引:鍵 -> 值在 .data 文件中的偏移量 private final Map<String, Long> indexMap = new HashMap<>(); // 空洞隊列:被刪除記錄留下的可復用槽位 private final Queue<Long> gaps = new ArrayDeque<>(); // 數據文件句柄 private RandomAccessFile db; // 元數據文件 private File metaFile; // 常量 private static final int MAX_DATA_LENGTH = 1020; private static final byte[] ZERO_BYTES = new byte[MAX_DATA_LENGTH]; private static final String DATA_SUFFIX = ".data"; private static final String META_SUFFIX = ".meta";
public BasicDB(String path, String name) throws IOException {
File dataFile = new File(path + name + DATA_SUFFIX);
metaFile = new File(path + name + META_SUFFIX);
db = new RandomAccessFile(dataFile, "rw"); // 讀寫模式
if (metaFile.exists()) {
loadMeta(); // 現有數據庫:加載索引與空洞
} else {
indexMap = new HashMap<>();
gaps = new ArrayDeque<>();
}
}
public void put(String key, byte[] value) throws IOException {
Long index = indexMap.get(key);
if (index == null) { // 新鍵:尋找槽位
index = nextAvailablePos();
indexMap.put(key, index);
}
writeData(index, value); // 覆寫或新寫
}
private long nextAvailablePos() throws IOException {
return gaps.isEmpty() ? db.length() : gaps.poll(); // 優先復用空洞
}
private void writeData(long pos, byte[] data) throws IOException {
if (data.length > MAX_DATA_LENGTH)
throw new IllegalArgumentException("Value too long: " + data.length);
db.seek(pos);
db.writeInt(data.length); // 4 字節長度
db.write(data); // 實際數據
db.write(ZERO_BYTES, 0, MAX_DATA_LENGTH - data.length); // 填充到固定槽位
}
public byte[] get(String key) throws IOException {
Long index = indexMap.get(key);
return index != null ? getData(index) : null;
}
private byte[] getData(long pos) throws IOException {
db.seek(pos);
int length = db.readInt();
byte[] data = new byte[length];
db.readFully(data); // 保證讀滿
return data;
}
public void remove(String key) {
Long index = indexMap.remove(key);
if (index != null) {
gaps.offer(index); // 標記為可復用,不立即清空磁盤
}
}
public void flush() throws IOException {
saveMeta(); // 寫入 .meta 文件
db.getFD().sync(); // 強制數據文件刷盤
}
private void saveMeta() throws IOException {
try (DataOutputStream out = new DataOutputStream(
new BufferedOutputStream(new FileOutputStream(metaFile)))) {
saveIndex(out);
saveGaps(out);
}
}
private void saveIndex(DataOutputStream out) throws IOException {
out.writeInt(indexMap.size());
for (Map.Entry<String, Long> e : indexMap.entrySet()) {
out.writeUTF(e.getKey());
out.writeLong(e.getValue());
}
}
private void saveGaps(DataOutputStream out) throws IOException {
out.writeInt(gaps.size());
for (Long pos : gaps) out.writeLong(pos);
}
private void loadMeta() throws IOException {
try (DataInputStream in = new DataInputStream(
new BufferedInputStream(new FileInputStream(metaFile)))) {
loadIndex(in);
loadGaps(in);
}
}
private void loadIndex(DataInputStream in) throws IOException {
int size = in.readInt();
indexMap = new HashMap<>((int)(size / 0.75f) + 1, 0.75f); // 預分配避免擴容
for (int i = 0; i < size; i++) {
indexMap.put(in.readUTF(), in.readLong());
}
}
private void loadGaps(DataInputStream in) throws IOException {
int size = in.readInt();
gaps = new ArrayDeque<>(size);
for (int i = 0; i < size; i++) gaps.add(in.readLong());
}
public void close() throws IOException {
flush();
db.close();
}
// 對象 -> byte[] (可用序列化、Protobuf、自定義編碼)
private static byte[] toBytes(Student s) throws IOException {
ByteArrayOutputStream bout = new ByteArrayOutputStream();
DataOutputStream dout = new DataOutputStream(bout);
dout.writeUTF(s.getName());
dout.writeInt(s.getAge());
dout.writeDouble(s.getScore());
return bout.toByteArray();
}
// 批量寫入
public static void saveStudents(Map<String, Student> students) throws IOException {
BasicDB db = new BasicDB("./", "students");
for (Map.Entry<String, Student> e : students.entrySet()) {
db.put(e.getKey(), toBytes(e.getValue()));
}
db.close(); // 自動 flush
}
| 特性 | 表現 |
|---|---|
| 內存佔用 | 極低(僅索引與空洞,值不進內存) |
| 讀寫延遲 | O(1) 隨機訪問,極快 |
| 存儲上限 | 受限於文件系統單文件大小,單值 ≤ 1020 字節(可調) |
| 並發安全 | 無(單線程設計,多線程需外部加鎖) |
| 崩潰恢復 | 無 WAL,flush() 前掉電會丟數據 |
| 範圍查詢 | 不支持(Hash 索引,無序) |
seek 系統調用開銷。DataOutputStream,簡化代碼並提升編解碼性能。GitHub:https://github.com/swiftma/program-logic
訪問文件還有一種方式——內存映射文件。它有什麼特點?何時比 RandomAccessFile 更優?讓我們繼續探索。