承 87 — Object Input Output including Serialization
Locations.java 寫出的部份
public static void main(String[] args) throws IOException { try (RandomAccessFile raOut = new RandomAccessFile("src/introduceIo/randomAccessFile/locations_raf.dat", "rwd")) { // The whole contents were consisted of A part, B part, C part, data of locations.
// A part 0 ~ 3 byte
// B part 4 ~ 7 byte
// C part 8 ~ 1699 bytes
// data of locations 1700 ~ end // A part - total number of locations which is 141
raOut.writeInt(locations.size()); // B part - start place of location's data, which is after A + B + C part
int indexSize = locations.size() * 3 * Integer.BYTES; // length of C part
int locationStart = (int) (indexSize + raOut.getFilePointer() + Integer.BYTES); // length of A + B + C part
System.out.println("locationStart: " + locationStart + ", which is the place of location's data start.");
raOut.writeInt(locationStart); // record place of C part start
long indexStart = raOut.getFilePointer();
System.out.println("indexStart " + indexStart + ", which is the place of C part start."); // measure every location's length and write all location's data
int startPointer = locationStart;
raOut.seek(startPointer); // jump to place of location's data start for (Location location : locations.values()) {
raOut.writeInt(location.getLocationID());
raOut.writeUTF(location.getDescription()); // retrieve Map of exits data (Map<String, Integer> exits)
StringBuilder stringBuilder = new StringBuilder();
for (String direction : location.getExits().keySet()) {
if (!direction.equalsIgnoreCase("Q")) {
stringBuilder.append(direction); // get exits key
stringBuilder.append(",");
stringBuilder.append(location.getExits().get(direction)); // get exits value
stringBuilder.append(",");
// would like W,2,E,3,S,1,....
}
}
raOut.writeUTF(stringBuilder.toString()); // calculate each of location length
int length = (int) (raOut.getFilePointer() - startPointer); // end of location - start of location
IndexRecord record = new IndexRecord(startPointer, length); index.put(location.getLocationID(), record); // put result to index map, so later C part could use it
startPointer = (int) raOut.getFilePointer(); // when current location was recorded, move pointer to the end of current location
} // C part - index of location
raOut.seek(indexStart); // back to C part start
for (Integer locationID : index.keySet()) {
raOut.writeInt(locationID);
raOut.writeInt(index.get(locationID).getStartByte());
raOut.writeInt(index.get(locationID).getLength());
}
}
}
IndexRecord.java
package introduceIo.randomAccessFile;public class IndexRecord {
private int startByte;
private int length; public IndexRecord(int startByte, int length) {
this.startByte = startByte;
this.length = length;
} public int getStartByte() {
return startByte;
} public void setStartByte(int startByte) {
this.startByte = startByte;
} public int getLength() {
return length;
} public void setLength(int length) {
this.length = length;
}
}
RandomAccessFile
意思就是當檔案很大時,不用把所有資料從文檔讀到記憶體去,如果像現在幾百筆資料其實沒什麼,但如果大到幾千萬、幾億筆資料呢?而隨機訪問就是按需要,有需要再去讀取特定位置的資料即可,這樣記憶體的開銷就會大幅降低。
簡單的講就是做一個 index(目錄的概念),當我要訪問第 X 筆資料時,就去查看目錄這筆資料記載第幾頁,然後翻到該頁查看即可。
搭配下方同學熱心補充的圖片即可了解,而我也在代碼裡面放很多註解了
簡單的說,一共有四個部份
A part — 資料總數
B part — 實際資料開始的位置
C part — 實際資料的 index
D part — 實際資料
邏輯跟順序:
A、B part 都只是簡單計算很簡單
C part 這是此案例較難的地方,因為每筆資料的長度都不同,如果所有資料長度都一樣,寫個長度公式就好,例如一筆資料長度 10 byte,取第十筆時,只要乘上 10 就好。
所以是先處理 D part,for 循環實際資料,同時計算長度跟寫入,然後把取得的各筆資料長度存到 Map 裡,最後才用 Map 取得的資料來寫到 c part。
最難的地方就是指標,我還以為 Java 不用學這個呢,以為只有 C 要學,沒想到在這邊遇上了,這邊的指標意思就是告訴系統,我現在要去哪個位置。
也可以當作書籤來理解,我可以在 A 處紀錄一個書籤,B 處紀錄一個書籤,此刻我想先到B 書籤查看跟書寫資料,處理好之後想回到 A 書籤去查看跟書寫,只要用 seek
告訴系統回到哪個書籤就行。
也是計算資料長度的好幫手,寫完一筆資料就移動指標到此資料結尾處,只要把此筆結尾減掉上一筆資料結尾,就會是現在這筆資料的長度。
心得:我在這個小節卡了超久,一度懷疑人生,前期看不懂是非常正常。我也是聽 Tim 老師講解聽得迷迷糊糊,心想到底在公三小朋友,讀得非常痛苦。能說的就是讀累了隔天再看讓腦袋放鬆,這樣重複個幾天讓腦袋重開機幾次後,就會慢慢越來越理解。
小技巧,當我實在聽不懂老師在說三小的時候,就會只看代碼反推老師的思路,同時搭配 debug 功能去看各個參數跑出什麼資料。
同學補充:
Locations.java 讀取的部份
static {
try {
raIn = new RandomAccessFile("src/introduceIo/randomAccessFile/locations_raf.dat", "rwd");
int numLocations = raIn.readInt();
long locationStart = raIn.readInt(); while (raIn.getFilePointer() < locationStart) {
int locationID = raIn.readInt();
int locationsStart = raIn.readInt();
int locationsLength = raIn.readInt(); IndexRecord record = new IndexRecord(locationsStart, locationsLength);
index.put(locationID, record);
}
} catch (IOException e) {
System.out.println("IOException in static initializer " + e.getMessage());
} }
怎麼寫出的,就怎麼讀入,相對簡單很多
Locations.java 新增功能
public Location getLocation(int locationID) throws IOException {
IndexRecord record = index.get(locationID);
raIn.seek(record.getStartByte());
int id = raIn.readInt();
String description = raIn.readUTF();
String exits = raIn.readUTF();
String[] exitPart = exits.split(","); Location location = new Location(id, description, null); if (locationID != 0) {
for (int i = 0; i < exitPart.length; i++) {
String direction = exitPart[i];
int destination = Integer.parseInt(exitPart[++i]);
location.addExit(direction, destination);
}
}
return location;
}public void close() throws IOException {
raIn.close();
}
因為不像之前全部讀到記憶體去了,現在是按需求讀取特定資料
所以新增查看特定資料的功能還有關閉 RandomAccessFile
的功能
Main.java
package introduceIo.randomAccessFile;import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Scanner;public class Main {
private static final Locations locations = new Locations();
private static final int startRoom = 1; public static void main(String[] args) throws IOException {
// Change the program to allow players to type full words, or phrases, then move to the
// correct location based upon their input.
// The player should be able to type commands such as "Go West", "run South", or just "East"
// and the program will move to the appropriate location if there is one. As at present, an
// attempt to move in an invalid direction should print a message and remain in the same place.
//
// Single letter commands (N, W, S, E, Q) should still be available. Scanner scanner = new Scanner(System.in); Map<String, String> vocabulary = new HashMap<String, String>();
vocabulary.put("QUIT", "Q");
vocabulary.put("NORTH", "N");
vocabulary.put("SOUTH", "S");
vocabulary.put("WEST", "W");
vocabulary.put("EAST", "E");
Location currentLocation = locations.getLocation(startRoom);
while (true) {
System.out.println(currentLocation.getDescription()); if (currentLocation.getLocationID() == 0) {
break;
} Map<String, Integer> exits = currentLocation.getExits();
System.out.print("Available exits are ");
for (String exit : exits.keySet()) {
System.out.print(exit + ", ");
}
System.out.println(); String direction = scanner.nextLine().toUpperCase();
if (direction.length() > 1) {
String[] words = direction.split(" ");
for (String word : words) {
if (vocabulary.containsKey(word)) {
direction = vocabulary.get(word);
break;
}
}
} if (exits.containsKey(direction)) {
currentLocation = locations.getLocation(currentLocation.getExits().get(direction));
} else {
System.out.println("You cannot go in that direction");
}
}
locations.close();
}
}
修改對應讀取 Location 的方法