:::info <a href="/@TFA101/CherryJava" target="_self"> < :notebook_with_decorative_cover: [共享目錄] Java 2 學習筆記</a> ::: # 模組12 物件輸入與輸出、序列化與反序列化 物件輸入、輸出是console I/O,屬於位元資料流,Reader和Writer不能直接存取(雖然可以[用 InputStreamReader 和 OutputStreamWriter 轉接](https://hackmd.io/@RtaWwakYTeOSfTzbFkNCTw/rk9JOkwQd))。 ## 物件輸入與輸出的類別:位元資料流 如上述,單純就類別來說,沒有 ObjectReader 跟 ObjectWriter 這兩種東西,**只有 ObjectInputStream 和 ObjectOutputStream**。 ### 類別、建構子與方法 以表格來看可能比較快: | 動作 | 輸入 | 輸出 | | -------------------- | ------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------- | | 類別 | ObjectInputStream | ObjectOutputStream | | 建構子 | ObjectInputStream(InputStream in) throws IOException | ObjectInputStream(OutputSteam out) throws IOException | | 方法(運用多型特徵) | <font color=red>Object</font> readObject() throws IOException, <font color=red>ClassNotFoundException</font> | void writeObject(Object obj) throws IOException | 關於ObjectInputStream 的 `readObject();` 方法,需注意幾點: 1. ClassNotFoundException 只有在所有 classpath 中(核心、擴充、自訂類別函式庫)都找不到類別才會發生。 2. `readObject();` 是將呼叫此方法的物件加工並轉型成 Object 再傳回去,所以若還想要使用原本型別的方法,**必須強制轉型回去**: ```java=4 // 前略... FileInputStream fis = new FileInputStream("file.ser"); // file.ser存放Book類別(透過實作Serializable空介面)序列化輸出的物件(包含屬性和方法) // 序列化與反序列化容後再述 // 但以下的操作就是最單純的反序列化 ObjectInputStream ois = new ObjectInputStream(fis); ((Book)ois.readObject()).show(); // readObject(); 將 ois 轉型為Object // 如果不強制轉型回原本的型別,將無法使用Book的方法show(); ``` ## 序列化(實作 Serializable 空介面) 若要將物件的資料,能與 OutputStream 類別串接並輸出,必須實作 Serializable 空介面: ```java= public class Xxx implements Serializable { // 實作這個空介面,貼上「可以被序列化」的標籤 } ``` ### 注意事項 1. **Java類別預設不實作空介面**(~~廢話~~) 2. **Java類別實作 Serializable 介面,其子類別會自動實作**(~~廢話~~) 3. **若是被序列化的物件裡還有物件的屬性,則這些物件也都必須可以被序列化** <!-- (不然不會被存出,讀取時會是屬性變數的型別預設值) --> ### 操作步驟 1. **實作Serializable 空介面** 幫這個類別貼上「可以被序列化」的標籤。 3. **建立一個低階I/O輸出資料流物件** 既然要輸出成檔案,還是要靠輸出節點資料流物件串接目的地,我們說的就是 FileOutputStream 建構子,設定「輸出檔案的路徑與檔案名」作為目的地。 3. **建立一個高階I/O輸出資料流物件(建構子包住低階I/O物件所產生)** 操作物件的輸出資料流當然是 ObjectOutputStream。 4. **使用高階I/O輸入資料流操作各種功能** 在這邊指的當然就是輸出資料了,使用的方法為表格中提到 `writeObject(Object obj);` 。 類別下的物件通常不只有一個,操作 while 迴圈反覆輸出,直到完整輸出所有物件,也是在這一個步驟。 5. **關閉資料流** 不再說明。 ### 操作範例 因為包含實作的標籤,不太好分解動作,完整的操作範例寫在這邊。 ```java= // 略過欲輸出類別的內容(屬性、方法、建構子)... public class Test implements Serializable { Book[] books = new Book[2]; // 略過把Book物件存進Book陣列books的動作... FileOutputStream fos = new FileOutputStream("輸出物件路徑\\物件名稱.ser"); // .ser 是Java建議存取物件檔案的副檔名 ObjectOutputStream oos = new ObjectOutputStream(fos); for (int i = 0; i < books.length; i++) oos.writeObject(books[i]); oos.close(); fos.close(); } ``` ## 反序列化 把物件拆成一小塊、一小塊的,還原回去當然需要依靠原本的設計圖(類別及其屬性和方法)。 原則上反序列化和序列化,比照輸入、輸出的觀念,正好相反。 **前提是在類別本身沒有更動的情況下**。如果類別變動了,物件反序列化就會出現例外(依然是[ClassNotFoundXException](https://hackmd.io/@RtaWwakYTeOSfTzbFkNCTw/SytKBnvXO#類別、建構子與方法))。 此機制其實是由 Java 透過檢查隱形的實體變數 `serialVersionUID` 在做判斷的,一旦類別經過改動, `serialVersionUID` 就會跟著改變(即使再改回去存檔也是新的!)。 ### serialVersionUID 在序列化物件的同時,Java 會自動在該物件類別底下加入 `serialVersionUID` 的屬性,值為隨機產生。 反序列化進行前,Java 會比對來源UID是否相同,確保輸入回來的資料流類別資訊是一致的,一旦不同,就禁止反序列化。 ### 維持反序列化的相容性 為了維持反序列化的相容性,必須由我們先發制人,主動宣告此資料並固定UID值,以確保日後類別內容有調整,對反序列化仍然有相容性。做法如下: ```Java= public Class Example { private static fianl serialVersionUID = -1; // 其實隨便你想寫什麼數字都可以 } ``` 值的內容真的是隨便,反正只要比對檢查輸入物件的UID來源跟目前的類別是一致的就好。