天天看點

【筆記】Java序列化機制解析

  • 首先建立三個類,A、B和C,這三個類均有兩個個int域,其中C還包含有一個A,代碼如下所示,構造器及getter/setter省略,因為方法不參與序列化,是以以下讨論不包含方法。

public class A {     private int priA;     public int pubA; } public class B {     private int priB;     public int pubB; } public class C extends B{     private int priC;     public int pubC;     public A a; } public class Test {     public static void main(String[] args) throws  IOException, ClassNotFoundException {          String fileName = "serial.dat";          A a = new A(1, 2);          C c = new C(2, 3, 4, 5, a);//構造一個對象C          System.out.println("=======serialize  C=======");          FileOutputStream fou = new  FileOutputStream(fileName);          ObjectOutputStream objout = new  ObjectOutputStream(fou);          objout.writeObject(c);          objout.close();          System.out.println("========deserialize  C========");          FileInputStream fin = new  FileInputStream(fileName);          ObjectInputStream objin = new  ObjectInputStream(fin);          C nc = (C) objin.readObject();          System.out.println("========compare  c&nc=========");          System.out.println("c==nc: " + (c == nc));          System.out.println("c.equals(nc): " +  (c.equals(nc)));          System.out.println("original c: " +  c.toString());          System.out.println("nc: " + nc.toString());     } }

  • java.io.Serializable接口

  •     該接口是Java序列化的預設接口,該方法沒有任何需要實作的抽象函數,僅僅是一個标記接口,代表目前類及其派生類支援序列化。并且目前類所有參與序列化的域都必須實作該接口。否則會報不支援序列化的錯誤
    • 【筆記】Java序列化機制解析
  • 然後讨論可序列化的資料
  1. 目前類中沒有被transient修飾的資料都會參加序列化。
  2. 父類中的資料是否參加序列化視情況而定,父類也實作了Serializable接口時,父類的資料也會參與序列化
      • 【筆記】Java序列化機制解析
    •     當父類沒有實作Serializable接口時,父類資料不參與序列化,并且必須擁有一個子類可通路的無參構造器(public或protected)。
      •     當缺少子類可見的無參構造器時會報沒有有效的構造器的錯誤
        • 【筆記】Java序列化機制解析
      •     有子類可見的無參構造器時,目前類正常序列化,通過調用的父類預設構造器來初始化父類的域
        • 【筆記】Java序列化機制解析

小節

  1. 預設的序列化機制在恢複對象時直接使用二進制資料來生成一個新的對象,不調用目前對象的構造器。
  2. 從目前類一直到上溯到Object類,所有實作了Serializable接口的類的域都參與序列化,未實作Serializable接口的類需要提供一個子類可見的無參構造器,jvm使用該構造器來初始化未實作Serializable接口的類的域。

自定義序列化過程

  •     當需要對序列化過程進行控制,例如在序列化時寫日志,對某些域不支援序列化,此時可以人為的将這些域的狀态儲存下來,然後在逆序列化時将其恢複。
  •     transient關鍵字:被該關鍵字标記的域會被自動序列機制忽略。
  •  private void writeObject(ObjectOutputStream out)  throws IOException和private void  readObject(ObjectInputStream in) throws  ClassNotFoundException, IOException方法
    •     這兩個方法是比對的,一般來說實作了一個就必須實作另一個,非transient域可以使用該方法按照自動序列化的方式進行序列化
    •     ObjectInputStream和ObjectOutputStream包含兩個方法,in.defaultReadObject()和out.defaultWriteObject();這兩個方法也是自動序列化機制預設調用的方法。
      •     defaultReadObject()方法從輸入流讀取資料,按照目前類的繼承層次,從上到下的執行各個類的defaultReadObject(),以此恢複各個類的域(不直接對父類進行資料恢複)。
      •     defaultWriteObject()的執行順序與defaultReadObject()相同,該方法隻管目前類的域的序列化(不直接序列化父類的域)
    • 【筆記】Java序列化機制解析
  •     對于transient域的序列化需要手動進行
    •     transient域不參與對象序列化,在對象逆序列化時會被設為虛拟機的初始化值(0或null)。
      •     将A類的pubA标記為transient,執行結果如下
      • 【筆記】Java序列化機制解析
    • 對transient域的手動序列化通過直接調用輸入輸出流的相關寫入寫出方法來執行
      • 【筆記】Java序列化機制解析
      • 注意序列化和逆序列化的順序是相同的。執行結果如下,此時手動控制了pubA的序列化和逆序列化。
      • 【筆記】Java序列化機制解析
  • private void readObjectNoData() throws ObjectStreamException;
    • 該方法也是序列化機制的一個方法,該方法不向資料流發送資料,也不從資料流中讀入資料,該方法隻負責在逆序列化之前對目前類的域設定初始化值。
    • 對一個類進行序列化,随後對該類進行拓展(extends),然後再對該類逆序列化,因為父類的資料不在序列化流中,是以無法對父類資料進行恢複,此時有以下三種情況:如果父類沒有實作Serializable接口,那麼使用無參構造器對父類資料進行初始化;如果父類實作了Serializable接口但是沒有實作readObjectNoData()方法,那麼使用虛拟機預設初始化值來初始化父類資料;如果父類同時實作了Serializable接口和readObjectNoData()方法,那麼通過執行readObjectNoData()方法對父類資料進行初始化。
      • 【筆記】Java序列化機制解析
    • 通俗的來說,readObjectNoData()方法負責在逆序列化時對定義該方法的類進行預設初始化。
  • 兩個不常用方法:ANY-ACCESS-MODIFIER Object writeReplace() throws ObjectStreamException 和ANY-ACCESS-MODIFIER Object readResolve() throws ObjectStreamException
    • readResolve()方法在逆序列化完成後,重建對象傳回前執行,即可以在逆序列化完成後通過該方法對對象進行某些操作,常見的用來保證單例和枚舉類型(通過靜态常量自定義的枚舉類型)的唯一性,避免逆序列化導緻出現多個單例對象。java語言的enum枚舉類型可以保證序列化和逆序列化過程中的唯一性,不需要通過該方法保證
      • 【筆記】Java序列化機制解析
      • 【筆記】Java序列化機制解析
    • writeReplace()方法用來傳回一個對象,該對象會替換掉要被序列化的對象,最終被寫入到序列化流中
      • 【筆記】Java序列化機制解析
      • 【筆記】Java序列化機制解析
      • 【筆記】Java序列化機制解析
      • 在該測試中,writeReplace()方法建立了一個list對象,然後将對象a儲存到list中,然後傳回list,此時自動序列化機制會将list對象寫入到輸出流中,即執行list對象及其内部成員的序列化方法。
        • 注意,因為在序列化list時,其内部成員也需要list,是以不能将目前類的執行個體存在該方法傳回的list中,否則會陷入無限循環導緻程式溢出,即使不溢出也會導緻程式出錯。即list.add(this)是非法的,會導緻this.writeReplace()方法不斷被執行。
      • 此時被序列化的對象不再是C,雖然輸出語句表明要序列化C,但是實際上被序列化的對象是writeReplace()方法傳回的對象(在這裡是包含a的list),是以通過ObjectInputStream流進行對象恢複時,讀取到的也是list。
      • C類中沒有任何方法可以解析writeReplace()方法傳回的對象,比如這裡是list,讀回資料時,讀到的也是list,List類的逆序列化方法會被執行,此時C類的所有逆序列化方法都不再起作用,讀回方法如測試代碼中紅框所示
  • Externalizable接口
    • Externalizable接口提供了對序列化和逆序列化的完全控制,該接口是Serializable的子接口。
    • 該接口包含兩個方法,分别用來讀入和寫入對象狀态:
      • 【筆記】Java序列化機制解析
    • 基本規則:
      • 使用Externalizable時,序列化和逆序列化時類的UID必須相同,否則會報錯。
      • 一旦使用了Externalizable接口,對象所有狀态的序列化完全被該接口内的方法接管,自動序列化機制不再執行,即隻執行目前類的Externalizable接口方法,目前類的所有域,無論是自己的還是從父類繼承過來的,都需要在目前類内通過Externalizable接口方法進行序列化和逆序列化。當然通用做法應該是調用父類的Externalizable接口方法來序列化父類的域,除非父類沒有實作Externalizable接口,此時需要目前類來對父類狀态進行必要的序列化
        • 【筆記】Java序列化機制解析
  • 版本控制
    • 序列化對象時通過類的serialVersionUID來表明版本。
    • serialVersionUID是類的公共靜态常量,如果被顯式定義了的話,會使用定義的值;如果沒有被定義,則虛拟機會根據Class檔案來進行計算。
    • 當兩個Class檔案的serialVersionUID不同的話,虛拟機會拒絕執行逆序列化
      • 【筆記】Java序列化機制解析
    • 通過顯式聲明可以保證虛拟機不會拒絕逆序列化,此時虛拟機會“盡力”将資料比對到執行逆序列化端的類檔案
      • 修改方法和靜态資料域不會對逆序列化産生影響,因為這兩部分不參與序列化過程
      • 修改資料域在代碼中的順序不會對逆序列化過程産生影響。
      • 修改變量名或者新增變量名會對逆序列化産生影響,被修改或者新增的變量會被初始化為預設值
        • 【筆記】Java序列化機制解析
        • 修改了priC的名稱後,可以看到逆序列化後的priC的資料丢失,說明逆序列化機制無法比對對變量名稱的修改。
      • 删除變量不會影響其餘變量的逆序列化過程
      • 修改變量資料類型會對逆序列化過程産生影響,虛拟機不會自動執行對象的轉換,預設兩個同名變量不比對,導緻逆序列化失敗
        • 【筆記】Java序列化機制解析
    • 總結:逆序列化時根據變量的名稱進行比對,當名稱比對類型不比對時,逆序列化失敗;當名稱不比對時,不執行該資料的逆序列化過程;對于目前類中新增的變量,由虛拟機初始化為預設值;對于序列化輸入流中多出來的變量資料,因為沒有變量比對是以會丢棄。

繼續閱讀