Java基礎(八)——IO流3_對象流

一、對象流

1、序列化與反序列化

  序列化:將記憶體中的Java對象保存到磁碟中或通過網路傳輸出去。
  反序列化:將磁碟文件中的對象還原為記憶體中的一個Java對象。

  用途:
  (1)將對象保存到物理硬碟:比如Web伺服器中的Session對象,當有10萬用戶並發訪問時,有可能出現10萬個Session對象,記憶體可能吃不消,從而導致OOM。於是Web容器就會把一些Session序列化到硬碟中,等需要時,再把硬碟中的對象反序列化到記憶體中。
  (2)將對象在網路上進行傳輸:當兩個進程進行通訊時,數據都會以二進位序列的形式在網路上進行傳輸。發送方需要把這個Java對象轉換為位元組序列,才能在網路上傳輸;接收方則需要把位元組序列再恢復為Java對象。

2、ObjectOutputStream、ObjectInputStream

  位元組流,處理流。ObjectOutputStream 和 ObjectInputStream,用於存儲和讀取基本數據類型對象的處理流。它的強大之處就是可以把Java中的對象寫入到數據源中,也能把對象從數據源中還原回來。
  序列化:用ObjectOutputStream類保存基本類型數據或對象的機制。
  反序列化:用ObjectInputStream類讀取基本類型數據或對象的機制。
  註:不能序列化statictransient修飾的成員變數。

  如何進行序列化?
  (1)必須實現介面:Serializable,這是一個標識介面,沒有任何抽象方法,用於表明該類是可序列化的。
  (2)定義一個全局常量:serialVersionUID,這個常量是可選的,用於標識類的版本號。
  (3)若類有類屬性:必須保證該類的所有屬性也是可序列化的。
  程式碼示例:標準模板

1 public class Person implements Serializable {
2     private static final long serialVersionUID = 1L;
3 }

  程式碼示例:序列化與反序列化

 1 // 序列化.對象->磁碟
 2 public class Main {
 3     public static void main(String[] args) {
 4         try (FileOutputStream stream = new FileOutputStream("Obj.dat");
 5              // 對象輸出流
 6              ObjectOutputStream oos = new ObjectOutputStream(stream);) {
 7 
 8             oos.writeObject("用於測試序列化");
 9             oos.writeObject(new Person(1001, "張三", new Account(11.1)));
10         } catch (Exception e) {
11         }
12     }
13 }
14 
15 // 反序列化.磁碟->對象
16 public class Main {
17     public static void main(String[] args) {
18         try (FileInputStream stream = new FileInputStream("Obj.dat");
19              // 對象輸入流
20              ObjectInputStream ois = new ObjectInputStream(stream);) {
21 
22             String str = (String) ois.readObject();
23             Person p = (Person) ois.readObject();
24 
25             System.out.println(str);
26             System.out.println(p);
27         } catch (Exception e) {
28         }
29     }
30 }
31 
32 
33 class Person implements Serializable {
34     public static final long serialVersionUID = 1L;
35     private int id;
36     private String name;
37     // private transient double hight; // 不需要序列化
38     private Account acct; // acct屬性必須也是可序列化的
39 
40     // 無參構造器
41     // 有參構造器
42     // getter & setter
43     // toString()
44 }
45 
46 class Account implements Serializable {
47     public static final long serialVersionUID = 1L;
48     private double balance;
49 
50     // 無參構造器
51     // 有參構造器
52     // getter & setter
53     // toString()
54 }

  總結(重要):
  (1)若未實現介面Serializable,會直接報java.io.NotSerializableException異常。
  (2)實現Serializable,但不寫serialVersionUID,不會有異常,但是會有隱藏的問題。如果類已經序列化了,此時修改了類的結構,比如新增了一個屬性,再反序列化的時候會報錯。
  (3)如果類中某個屬性不想被序列化,可以加上關鍵字transient。

3、serialVersionUID的理解

  若沒有

  private static final long serialVersionUID = 1L;

  先序列化類,然後修改類的結構(如,新增一個欄位),再反序列化。會報錯如下:

  java.io.InvalidClassException: temp.file.Person; local class incompatible: stream classdesc serialVersionUID = 503624515100475858, local class serialVersionUID = -443494311322032311

  serialVersionUID:序​列​化​的​版​本​號​,凡是實現Serializable介面的類都有一個表示序列化版本標識符的靜態變數,用來表明類的不同版本間的兼容性。
如果類沒有顯示定義這個靜態變數,它的值是Java運行時環境根據類的內部細節自動生成的。若類的結構做了修改,文件流中的class和classpath中的class不兼容了,處於安全機制考慮,程式拋出異常,並拒絕載入。
  解決:上述問題只需要顯式聲明 serialVersionUID 即可。
  既然 serialVersionUID 是在序列化的時候使用到,那麼抽象類(abstract)沒有實例被序列化是不是就不需要定義 serialVersionUID 屬性呢?事實是序列化的時候會遞歸獲取父類的描述,所以如果父類的 serialVersionUID 修改了,同樣會導致子類對象反序列化失敗。