對象序列化與反序列化(Serializable、Externalizable )

對象序列化?

對象序列化的概念加入到語言中爲了提供對兩種主要特性的支持:

1 、遠程方法調用

2 、 Java Beans 狀態的保存與恢復

 

 

ObjectInput 接口繼承 DataInput 接口

ObjectOutput 接口繼承 DataOutput 接口

ObjectOutputStream 類實現了 DataOutput,ObjectOutput

ObjectInputStream 類實現了 DataInput,ObjectInput

 

 

ObjectOutputStream.defaultWriteObject() :將當前類的非靜態和非瞬態字段寫入此流。

ObjectInputStream.defaultReadObject() :從此流讀取當前類的非靜態和非瞬態字段。

 

反序列化時,該序列化對象所對應的類一定要在反序列化運行環境的classpath中找得到,不然在讀(readObject()) 序列化文件時因找不到相應的Class會拋出ClassNotFoundException異常。

 

一個類實現了序列化接口,則該類裏的所有對象都要求實現序列化接口,不然在進行序列化進會拋異常。因爲序列化好比深層克隆,它會序列化各個對象屬性裏的對象屬性。 如果一個屬性沒有實現可序列化,而我們又沒有將其用transient 標識, 則在對象序列化的時候, 會拋出java.io.NotSerializableException 異常。

使用 Serializable序列化

爲了序列化一個對象,首先要創建某些 OutputStream 對象,然後將其封裝在一個 ObjectOutput  Stream 對象內。這時,只需調用 writeObject() 即可將對象序列化,並將其發送給 OutputStream 。要將一個序列重組爲一個對象,需要將一個 InputStream 封裝在 ObjectInputStream 內,然後調用readObject() 。和往常一樣,我們最後獲得的是指向一個向上轉型爲 Object 的句柄,所以必須向下轉型,以便能夠直接對其進行設置。

 

以下示例演示了怎樣通過實現Serializable標記接口來實現序列化:

Java代碼  收藏代碼
  1. import java.io.ByteArrayInputStream;  
  2. import java.io.ByteArrayOutputStream;  
  3. import java.io.IOException;  
  4. import java.io.ObjectInputStream;  
  5. import java.io.ObjectOutputStream;  
  6. import java.io.Serializable;  
  7. import java.util.Random;  
  8.   
  9. //可序列化對象,實現了Serializable標誌接口  
  10. class Data implements Serializable {  
  11.     private int n;  
  12.   
  13.     public Data(int n) {  
  14.         this.n = n;  
  15.     }//默認構造器  
  16.   
  17.     public String toString() {  
  18.         return Integer.toString(n);  
  19.     }  
  20. }  
  21.   
  22. //可序列化對象,實現了Serializable標誌接口,並形成一個網絡對象  
  23. public class Worm implements Serializable {  
  24.     private static Random rand = new Random();  
  25.     //Worm對象裏有Data數組,數組裏又存放了Data對象  
  26.     private Data[] d = { new Data(rand.nextInt(10)), new Data(rand.nextInt(10)),  
  27.             new Data(rand.nextInt(10)) };  
  28.     private Worm next;//指向下一個Worm對象,如果是最後一個,則指向null  
  29.     private char c;  
  30.   
  31.     public Worm(int i, char x) {//i表示構造幾個這樣的蠕蟲對象,也即Worm對象的編號  
  32.         System.out.println("Worm constructor: " + i);  
  33.         c = x;  
  34.         if (--i > 0) {  
  35.             //指向的每個Worm對象的c爲起始的x增加一,比如傳進來  
  36.             //的x起始爲 A,則下一個Worm對象的x就爲B,依次類推  
  37.             next = new Worm(i, (char) (x + 1));  
  38.         }  
  39.     }  
  40.   
  41.     //默認構造函數  
  42.     public Worm() {  
  43.         System.out.println("Default constructor");  
  44.     }  
  45.   
  46.     //遞歸打印蠕蟲對象信息  
  47.     public String toString() {  
  48.         String s = ":" + c + "(";  
  49.         for (int i = 0; i < d.length; i++) {  
  50.             s += d[i];  
  51.             if (i != d.length - 1) {  
  52.                 s += " ";  
  53.             }  
  54.         }  
  55.         s += ")";  
  56.         //如果不是最後一個  
  57.         if (next != null) {  
  58.             s += next.toString();  
  59.         }  
  60.         return s;  
  61.     }  
  62.   
  63.     public static void main(String[] args) throws ClassNotFoundException, IOException {  
  64.         System.out.println("----開始構造Wrom");  
  65.         //構造6個蠕蟲對象,且x起始爲 A   
  66.         Worm w = new Worm(6'A');  
  67.         System.out.println("w = " + w);  
  68.   
  69.         System.out.println("----開始序列化");  
  70.         ByteArrayOutputStream bout = new ByteArrayOutputStream();  
  71.         ObjectOutputStream out2 = new ObjectOutputStream(bout);  
  72.         out2.writeObject("Worm storage\n");//存儲一個字符串  
  73.         out2.writeObject(w);//存儲Worm對象  
  74.         out2.flush();  
  75.         System.out.println("----開始反序列化");  
  76.         ObjectInputStream in2 = new ObjectInputStream(new ByteArrayInputStream(bout  
  77.                 .toByteArray()));  
  78.         String s = (String) in2.readObject();//讀字符串  
  79.         Worm w3 = (Worm) in2.readObject();//讀Worm對象  
  80.         System.out.println(s + "w3 = " + w3);  
  81.     }  
  82. }  

 

某次運行的結果如下:

----開始構造Wrom
Worm constructor: 6
Worm constructor: 5
Worm constructor: 4
Worm constructor: 3
Worm constructor: 2
Worm constructor: 1
w = :A(6 8 7):B(9 2 1):C(4 7 8):D(4 1 7):E(4 0 8):F(9 7 3)
----開始序列化
----開始反序列化
Worm storage
w3 = :A(6 8 7):B(9 2 1):C(4 7 8):D(4 1 7):E(4 0 8):F(9 7 3)

使用Externalizable序列化

缺省的序列化機制並不難操作。然而,如果有特殊的需要那又該怎麼辦?例如,也許你有考慮特殊的安全問題,而且你不希望對象的某一部分被序列化;或者一個對象被重組以後,某子對象需要重新創建,從而不必將該子對象序列化。這時,可通過實現 Externalizable 接口代替實現 Serializable接口來對序列化過程進行控制。這個 Externalizable 接口繼承了 Serializable 接口 ,同時增添了兩個方法:

readExternal(ObjectInput in) throws IOException,CalssNotFoundException

writeExternal(ObjectOutput out) throws IOException,CalssNotFoundException

這兩個方法會在序列化和重組的過程中被自動調用,以便執行一些特殊操作。

 

Serializable 對象完全以它存儲的二進制位爲基礎重組,反序列化時不會調用構造函數,哪怕是默認構造函數 。

 

而對一個 Externalizable 對象,反序列化時缺省構造函數先會被調用 ,然後調用 readExternal()

 

注:序列化方法(writeObject)不會自動對 transient 屬性與靜態的屬性序列化 ( 從 API 中ObjectOutputStream.  defaultWriteObject() 方法的描述就可知這個結論,其描述如下: Write the non-static and  non-transient fields of the current class to this stream.)

 

實現 Externalizable接口步驟 如下:

1 、實現 Externalizable 接口

2 、實現 writeExternal() ,在方法中指明序列化哪些對象,如果不存儲則不能保存某屬性狀態

3 、 實現 readExternal() ,在方法中指明反序列化哪些對象,如果不讀取則不能恢復某屬性狀態

 

Externalizable 恢復一個對象狀態過程如下:

1 、調用對象的缺省構造函數 ( 注:缺省構造函數一定要是 public 的 ,其他都不行,否在反序列化時出錯 )

2 、通過 readExternal() 對各個屬性進行進一步的恢復

 

下面這個例子示範瞭如何完整保存和恢復一個Externalizable對象:

Java代碼  收藏代碼
  1. import java.io.Externalizable;  
  2. import java.io.FileInputStream;  
  3. import java.io.FileOutputStream;  
  4. import java.io.IOException;  
  5. import java.io.ObjectInput;  
  6. import java.io.ObjectInputStream;  
  7. import java.io.ObjectOutput;  
  8. import java.io.ObjectOutputStream;  
  9.   
  10. public class Blip3 implements Externalizable {  
  11.     private int i;  
  12.     private String s; // 未初始化  
  13.   
  14.     //Externalizable反序列化時會先調用  
  15.     public Blip3() {  
  16.         System.out.println("Blip3 Constructor");  
  17.         // s, i 未初始化  
  18.     }  
  19.   
  20.     public Blip3(String x, int a) {  
  21.         System.out.println("Blip3(String x, int a)");  
  22.         s = x;  
  23.         i = a;  
  24.         // s & i 在非默認構造函數中初始化  
  25.     }  
  26.   
  27.     public String toString() {  
  28.         return s + i;  
  29.     }  
  30.   
  31.     public void writeExternal(ObjectOutput out) throws IOException {  
  32.         System.out.println("Blip3.writeExternal");  
  33.         // 序列化時你必須這樣做,你不能((ObjectOutputStream) out).defaultWriteObject();這樣  
  34.         out.writeObject(s);  
  35.         out.writeInt(i);  
  36.   
  37.     }  
  38.   
  39.     public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {  
  40.         System.out.println("Blip3.readExternal");  
  41.         // 反序列化時你必須這樣做,你不能((ObjectInputStream) in).defaultReadObject()這樣  
  42.         s = (String) in.readObject();  
  43.         i = in.readInt();  
  44.   
  45.     }  
  46.   
  47.     public static void main(String[] args) throws IOException, ClassNotFoundException {  
  48.         System.out.println("--Constructing objects:");  
  49.         Blip3 b3 = new Blip3("A String "47);  
  50.         System.out.println(b3);  
  51.         ObjectOutputStream o = new ObjectOutputStream(new FileOutputStream("Blip3.out"));  
  52.         System.out.println("--Saving object:");  
  53.         o.writeObject(b3);  
  54.         o.close();  
  55.         // Now get it back:  
  56.         ObjectInputStream in = new ObjectInputStream(new FileInputStream("Blip3.out"));  
  57.         System.out.println("--Recovering b3:");  
  58.         b3 = (Blip3) in.readObject();  
  59.         System.out.println(b3);  
  60.   
  61.     }  
  62. }  

--Constructing objects:
Blip3(String x, int a)
A String 47
--Saving object:
Blip3.writeExternal
--Recovering b3:
Blip3 Constructor
Blip3.readExternal
A String 47

可控的 Serializable 序列化( Externalizable的替代方案)

有一種防止對象的敏感部分被序列化的辦法,就是將我們自己的類實現爲 Externalizable ,這樣一來,沒有任何東西可以自動序列化,並且我們可以 writeExteranl() 內部只對所需要部分進行顯式的序列化。但是,如果我們正在操作的 確確實實是一個Serializable 對象,那麼所有序列化操作都會自動進行。爲了能夠控制,可以用 transient ( 瞬時 ) 關鍵字逐個地關閉序列化 ,它意旨 " 不用麻你或恢復數據 -- 我自己會處理的 " 。

 

由於Externalizable對象在缺省情況下不保存它們的任何域,所以transient關鍵字只能和Serializable對象一起使用 。

 

如果我們不是特別想要實現 Externalizable 接口,那麼就還有一種方法。我們可以實現 Serializable接口,並添加 ( 注:我說的是 " 添加 " ,而不是 " 實現 " 或 " 重載 ") 名爲 writeObject() 和readObject() 的方法。這樣一旦對象被序列化或都被反序列化,就會自動地分別調用這兩個方法。也就是說,只要我們提供了這兩個方法,就會使用它們而不是缺省的序列化機制 。這些方法必須具有準確的方法簽名:

private void writeObject(ObjectOutputStream stream) throws IOException;

private void readObject(ObjectInputStream stream) throws IOException,CalssNot  FoundException

從設計的觀點來看,現在事情變得真是不可思議。它們被定義成了 private ,這意思味着它們不能被這個類的其成員調用。然面,實際上我們並沒有從這個類的其他方法中調用它們,而是ObjectOutputStream 和 ObjectInputStream 對象的 writeObject() 和 readObject() 方法調用我們對象的 writeObject() 和 readObject() 方法 。 在你調用 ObjectOutputStream.writeObject() 時,會檢查你所傳遞的 Serializable 對象,看是否實現 ( 準確的說應該是添加 ) 了它自己的 writeObject() ,如果是這樣,就跳過正常的序列化過程並調用它的 writeObject() 。 readObject() 的情形與此相同。

 

還有另外一個技巧。在我們添加的 writeObject(ObjectOutputStream stream) 內部,可以調用defaultWriteObject() 來選擇執行缺省的 writeObject() 。類似地,在readObject(ObjectInputStream stream) 內部,我們可以調用 defaultReadObject() 。

 

注:如果某實現了 Serializable 接口並添加了 writeObject() 與 readObject() 方法的類,要保存非transient 部分,那麼我們必須調用 defaultWriteObject() 操作作爲 writeObject() 中的第一個操作,並讓 defaultReadObject() 作爲 readObject() 中的第一個操作,如果不這樣的話,我們只能手動一個個存儲與恢復了 。

 

下面示例演於瞭如何對一個Serializable對象的存儲與恢復進行控制:

Java代碼  收藏代碼
  1. import java.io.ByteArrayInputStream;  
  2. import java.io.ByteArrayOutputStream;  
  3. import java.io.IOException;  
  4. import java.io.ObjectInputStream;  
  5. import java.io.ObjectOutputStream;  
  6. import java.io.Serializable;  
  7.   
  8. /** 
  9.  * Serializable接口的自動序列化性爲的控制 
  10.  */  
  11. public class SerialCtl implements Serializable {  
  12.   
  13.     //非transient域可由defaultWriteObject()方法保存  
  14.     private String a;  
  15.     //transient域必須在程序中明確保存和恢復  
  16.     private transient String b;  
  17.   
  18.     //默認構造函數,反序列化時不會調用  
  19.     public SerialCtl() {  
  20.         System.out.println("defuault constructor");  
  21.     }  
  22.       
  23.     //構造函數,反序列化時不會調用  
  24.     public SerialCtl(String aa, String bb) {  
  25.         a = "Not Transient: " + aa;  
  26.         b = "Transient: " + bb;  
  27.     }  
  28.   
  29.     public String toString() {  
  30.         return a + "\n" + b;  
  31.     }  
  32.   
  33.     /*  
  34.      * 添加writeObject(ObjectOutputStream stream)私有方式, 
  35.      * 序列化時會自動由ObjectOutputStream對象的writeObject方法來調用 
  36.      */  
  37.     private void writeObject(ObjectOutputStream stream) throws IOException {  
  38.         //要在首行調用默認序列化方法  
  39.         stream.defaultWriteObject();  
  40.         //我們手工序列化那些調用默認序列化方法(defaultWriteObject)無法序列化的屬性  
  41.         stream.writeObject(b);  
  42.     }  
  43.   
  44.     /*  
  45.      * 添加readObject(ObjectInputStream stream)私有方式, 
  46.      * 序列化時會自動由ObjectInputStream對象的readObject方法來調用 
  47.      */  
  48.     private void readObject(ObjectInputStream stream) throws IOException,  
  49.             ClassNotFoundException {  
  50.         //要在首行調用默認序列化方法  
  51.         stream.defaultReadObject();  
  52.         //我們手工反序列化那些調用默認反序列化方法(defaultReadObject)無法反序列化的屬性  
  53.         b = (String) stream.readObject();  
  54.     }  
  55.   
  56.     public static void main(String[] args) throws IOException, ClassNotFoundException {  
  57.         SerialCtl sc = new SerialCtl("Test1""Test2");  
  58.         System.out.println("Before:\n" + sc);  
  59.         ByteArrayOutputStream buf = new ByteArrayOutputStream();  
  60.         ObjectOutputStream o = new ObjectOutputStream(buf);  
  61.         o.writeObject(sc);  
  62.         // Now get it back:  
  63.         ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(buf  
  64.                 .toByteArray()));  
  65.         SerialCtl sc2 = (SerialCtl) in.readObject();  
  66.         System.out.println("After:\n" + sc2);  
  67.   
  68.     }  
  69. }  

Before:
Not Transient: Test1
Transient: Test2
After:
Not Transient: Test1
Transient: Test2

另一種可控的 Serializable 序列化

具體請參考《java解惑你知多少(七)》中的【54. 實現Serializable的單例問題】

同一對象多次序列化到同一輸出流與不同輸出流

如果我們將兩個都具有指向第三個對象的引用的對象進行序列化,會發生什麼情況?

當我們從它們的序列化狀態恢復這兩個對象時,第三個對象會只出現一次嗎?

如果將這兩個對象序列化成獨立的文件,然後在代碼的不同部分對它們進行反序列化,又會怎樣呢?

請看本例分解:

Java代碼  收藏代碼
  1. import java.io.ByteArrayInputStream;  
  2. import java.io.ByteArrayOutputStream;  
  3. import java.io.IOException;  
  4. import java.io.ObjectInputStream;  
  5. import java.io.ObjectOutputStream;  
  6. import java.io.Serializable;  
  7. import java.util.ArrayList;  
  8. import java.util.List;  
  9.   
  10. class House implements Serializable {  
  11.     private static final long serialVersionUID = 7763424872069972808L;  
  12. }  
  13.   
  14. class Animal implements Serializable {  
  15.     private static final long serialVersionUID = 4585314037312913787L;  
  16.     private String name;  
  17.     private House preferredHouse;  
  18.   
  19.     public Animal(String nm, House h) {  
  20.         name = nm;  
  21.         preferredHouse = h;  
  22.   
  23.     }  
  24.   
  25.     public String toString() {  
  26.         return name + "[" + super.toString() + "]," + preferredHouse + "\n";  
  27.     }  
  28.   
  29.     public String getName() {  
  30.         return name;  
  31.     }  
  32.   
  33.     public void setName(String name) {  
  34.         this.name = name;  
  35.     }  
  36. }  
  37.   
  38. public class MyWorld {  
  39.     public static void main(String[] args) throws IOException, ClassCastException,  
  40.             ClassNotFoundException {  
  41.         House house = new House();  
  42.         List animals = new ArrayList();  
  43.         // 讓三種動物都引用同一個對象house  
  44.         animals.add(new Animal("Bosco the dog", house));  
  45.         animals.add(new Animal("Ralph the hamster", house));  
  46.         animals.add(new Animal("Fronk the cat", house));  
  47.   
  48.         System.out.println("animals:" + animals);  
  49.   
  50.         System.out.println("----開始序列化");  
  51.         ByteArrayOutputStream buf1 = new ByteArrayOutputStream();  
  52.         ObjectOutputStream o1 = new ObjectOutputStream(buf1);  
  53.         o1.writeObject(animals);  
  54.   
  55.         // 試着改變狀態  
  56.         ((Animal) animals.get(0)).setName("pig pig...");  
  57.   
  58.         /* 
  59.          * 再往同一輸出流序列化一次,實質上上面的改變狀態操作對這次序列化不會起作用,這裏存儲的 
  60.          * 對象狀態還是爲第一次序列化時的屬性那個狀態,但在改變狀態後,如果序列化到另一個輸出流 
  61.          * 中時,這時會以最新對象狀態來序列化 
  62.          */  
  63.         o1.writeObject(animals);//再存一次  
  64.         System.out.println(((Animal) animals.get(0)).getName());  
  65.   
  66.         // 序列化到另外一個流中  
  67.         ByteArrayOutputStream buf2 = new ByteArrayOutputStream();  
  68.         ObjectOutputStream o2 = new ObjectOutputStream(buf2);  
  69.         o2.writeObject(animals);  
  70.   
  71.         System.out.println("----開始反序列化");  
  72.         ObjectInputStream in1 = new ObjectInputStream(new ByteArrayInputStream(buf1  
  73.                 .toByteArray()));  
  74.         ObjectInputStream in2 = new ObjectInputStream(new ByteArrayInputStream(buf2  
  75.                 .toByteArray()));  
  76.         /* 
  77.          * 注,上面o1存儲了兩遍,現也同樣從in1讀取兩遍,同一流中如果對同一對象存儲多次後, 
  78.          * 在讀取時,也是同一對象 
  79.          */  
  80.         List animals1 = (List) in1.readObject();  
  81.         List animals2 = (List) in1.readObject();  
  82.   
  83.         // 如果輸入到不同的流中時,儘管是存儲的同一對象,但恢復過來時是不同的對象(內存地址不一樣)  
  84.         List animals3 = (List) in2.readObject();  
  85.   
  86.         System.out.println("animals1:" + animals1);  
  87.         System.out.println("animals2:" + animals2);  
  88.         System.out.println("animals3:" + animals3);  
  89.     }  
  90. }  

animals:[Bosco the dog[序列化.Animal@d9f9c3],序列化.House@9cab16
, Ralph the hamster[序列化.Animal@1a46e30],序列化.House@9cab16
, Fronk the cat[序列化.Animal@3e25a5],序列化.House@9cab16
]
----開始序列化
pig pig...
----開始反序列化
animals1:[Bosco the dog[序列化.Animal@156ee8e],序列化.House@47b480
, Ralph the hamster[序列化.Animal@19b49e6],序列化.House@47b480
, Fronk the cat[序列化.Animal@10d448],序列化.House@47b480
]
animals2:[Bosco the dog[序列化.Animal@156ee8e],序列化.House@47b480
, Ralph the hamster[序列化.Animal@19b49e6],序列化.House@47b480
, Fronk the cat[序列化.Animal@10d448],序列化.House@47b480
]
animals3:[pig pig...[序列化.Animal@e0e1c6],序列化.House@6ca1c
, Ralph the hamster[序列化.Animal@1bf216a],序列化.House@6ca1c
, Fronk the cat[序列化.Animal@12ac982],序列化.House@6ca1c
]

結果:這些反序列化的對象地址與原來對象的地址肯定是不同。但請注意,在animals1和 animals2中卻出現了相同的地址,包括兩者共享的那個指向house的引用。另一方面 當恢復naimals3時,系統無法知道另一個流內的對象是第一個流內對象的別名,因此 它會產生出完全不同的對象網。


只要我們將任何對象序列化到一個單一流中,我們就可以恢復出與我們寫出時一樣的對象 網,並且沒有任何意外重複複製出的對象。當然,我們可以在寫出第一個對象與寫出第二個對象期間改變這些對象的狀態,這種更新操作對存儲到同一流中的序列化操作不起作用; 只對更新狀態後再存儲到另一流中起作用 。

 

ObjectOutputStream.writeUnshared

ObjectOutputStream.writeUnshared:

 將“未共享”對象寫入 ObjectOutputStream。此方法等同於 writeObject,不同點在於它總是將給定對象作爲流中惟一的新對象進行寫入(相對於指向以前序列化實例的 back 引用而言)。尤其是:

  • 通過 writeUnshared 寫入的對象總是作爲新出現對象(未曾將對象寫入流中)被序列化,不管該對象以前是否已經被寫入過。
  • 如果使用 writeObject 寫入以前已經通過 writeUnshared 寫入的對象,則可將以前的 writeUnshared 操作視爲寫入一個單獨對象,即writeObject 會重新生成一個新的對象。換句話說,ObjectOutputStream 永遠不會生成通過調用 writeUnshared 寫入的對象數據的 back 引用。

雖然通過 writeUnshared 寫入對象本身不能保證反序列化對象時對象引用的惟一性,但它允許在流中多次定義單個對象,因此接收方對 readUnshared 的多個調用不會引發衝突。注意,上述規則僅應用於通過 writeUnshared 寫入的基層對象(被序列化對象本身),而不能應用於要序列化的對象圖形中的任何可變遷方式引用的子對象(即不會再次創建被序列化對象裏的成員對象)。

 

Java代碼  收藏代碼
  1. public class Test {  
  2.     private final static class V implements Serializable {  
  3.         StringBuffer sb = new StringBuffer();  
  4.     };  
  5.   
  6.     private static V v = new V();  
  7.   
  8.     public static void main(String[] args) throws Exception {  
  9.         testWriteUnshared(3);  
  10.     }  
  11.   
  12.     static void testWriteUnshared(int times) throws Exception {  
  13.         ByteArrayOutputStream bos1,bos2;  
  14.         ObjectOutputStream oos1,oos2;  
  15.         ByteArrayInputStream bin1,bin2;  
  16.         ObjectInputStream ois1,ois2;  
  17.   
  18.         bos1 = new ByteArrayOutputStream();  
  19.         oos1 = new ObjectOutputStream(bos1);  
  20.         bos2 = new ByteArrayOutputStream();  
  21.         oos2 = new ObjectOutputStream(bos2);  
  22.         for (int i = 0; i < times; i++) {  
  23.             v.sb.append(i);  
  24.             /* 
  25.              * 將同一個對象存儲到同一流時,writeUnshared只是對v本身進行了多個複製,但 
  26.              * 對象裏所引用的其他成員對象如這裏的sb是不會再次複製的,它會引用第一次向該 
  27.              * 流寫入時的同一個對象,所以當一個對象內部狀態改變後通writeUnshared寫入還 
  28.              * 是不能更新,它只不過寫入了另一個基層對象而已。 
  29.              */  
  30.             oos1.writeUnshared(v);  
  31.             //將同一對象寫入同一流時,對象不會重新寫入,而是引用第一次序列化後的對象  
  32.             oos2.writeObject(v);  
  33.         }  
  34.   
  35.         bin1 = new ByteArrayInputStream(bos1.toByteArray());  
  36.         ois1 = new ObjectInputStream(bin1);  
  37.         bin2 = new ByteArrayInputStream(bos2.toByteArray());  
  38.         ois2 = new ObjectInputStream(bin2);  
  39.         V v = null;  
  40.         for (int i = 0; i < times; i++) {  
  41.             v = (V) ois1.readUnshared();  
  42.             /* 
  43.              * 某次輸出結果: 
  44.              * Test$V@a62fc3 : 0  9023134 
  45.              * Test$V@1270b7 : 0  9023134 
  46.              * Test$V@60aeb0 : 0  9023134 
  47.              */  
  48.             System.out.println(v + " :- " + v.sb + "  " + v.sb.hashCode());  
  49.               
  50.             v = (V) ois2.readObject();  
  51.             /* 
  52.              * 某次輸出結果: 
  53.              * Test$V@60aeb0 : 0  23899971 
  54.              * Test$V@60aeb0 : 0  23899971 
  55.              * Test$V@60aeb0 : 0  23899971 
  56.              */  
  57.             System.out.println(v + " : " + v.sb + "  " + v.sb.hashCode());  
  58.         }  
  59.     }  
  60. }  

 

 

靜態與transient數據不可序列化

Java代碼  收藏代碼
  1. import java.io.FileInputStream;  
  2. import java.io.FileNotFoundException;  
  3. import java.io.FileOutputStream;  
  4. import java.io.IOException;  
  5. import java.io.ObjectInputStream;  
  6. import java.io.ObjectOutputStream;  
  7. import java.io.Serializable;  
  8.   
  9. class A implements Serializable {  
  10.     private static final long serialVersionUID = -4829544934963584924L;  
  11.     public static int i = prt();  
  12.     public transient int y = 33;  
  13.     public int j = prt(22);  
  14.   
  15.     //Serializable方式序列化時是不會調用構建函數的  
  16.     public A() {  
  17.         System.out.println("調用構建");  
  18.     }  
  19.   
  20.     public static int prt() {  
  21.         System.out.println("初使化靜態變量i");  
  22.         return 1;  
  23.     }  
  24.   
  25.     public static int prt(int j) {  
  26.         System.out.println("初使化變量j");  
  27.         return j;  
  28.     }  
  29.   
  30. }  
  31.   
  32. public class StaticTransientFeildSerial {  
  33.   
  34.     public static void main(String[] args) throws FileNotFoundException, IOException,  
  35.             ClassNotFoundException {  
  36.         if (args.length == 0) {  
  37.             // 開始序列化:  
  38.             ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(  
  39.                     "serial.dat"));  
  40.             A.i = 2;  
  41.             out.writeObject(new A());  
  42.             out.close();  
  43.         } else {  
  44.             // 反序列化:  
  45.             ObjectInputStream in = new ObjectInputStream(  
  46.                     new FileInputStream("serial.dat"));  
  47.   
  48.             // 默認恢復操作時,如果恢復的類在恢復前未加載進來過,則在恢復時會以定義時的值初始化  
  49.             // (如果值是調用某方法獲得的,則也會去調用初始化方法),如果加載過,則對它不進行任何  
  50.             // 操作,其實這是類加載時對靜態成員變量的初始化機制罷了。  
  51.             A a = (A) in.readObject();  
  52.             System.out.println("非靜態變量j=" + a.j);  
  53.             System.out.println("靜態變量i=" + A.i);  
  54.             System.out.println("transient變量y=" + a.y);  
  55.             in.close();  
  56.         }  
  57.     }  
  58. }  

序列化運行結果:

初使化靜態變量i
初使化變量j
調用構建


反序列化運行結果:

初使化靜態變量i
非靜態變量j=22 
       //注:可以看到在恢復非靜態變量j時沒有調用prt(22)方法,但狀態已恢復 
靜態變量i=1            //說明靜態變量沒能恢復過來,如果要恢復靜態變量的狀態只能手工序列化 
transient變量y=0    //說明transient變量沒能恢復過來,如果要恢復靜態變量的狀態只能手工序列化

 

所以,如果要對靜態成員與transient成員進行序列化時,我們只能通 Externalizable 或者是可控的Serializable 來實現。

 

哪此屬性不會被序列化?

並不是一個實現了序列化接口的類的所有字段及屬性都是可以序列化的:

  • 如果該類有父類,則分兩種情況來考慮,如果該父類已經實現了可序列化接口。則其父類的相應字段及屬性的處理和該類相同;如果該類的父類沒有實現可序列化接口,則該類的父類所有的字段屬性將不會序列化,並且反序列化時會調用父類的默認構造函數來初始化父類的屬性,而子類卻不調用默認構造函數,而是直接從流中恢復屬性的值。
  • 如果該類的某個屬性標識爲static類型的,則該屬性不能序列化。
  • 如果該類的某個屬性採用transient關鍵字標識,則該屬性不能序列化。

序列化類多重版本的控制

如果在反序列化的JVM 裏出現了該類的不同時期的版本,那麼我們該如何處理的呢?


爲了避免這種問題,Java的序列化機制提供了一種指紋技術,不同的類帶有不同版本的指紋信息,通過其指紋就可以辨別出當前JVM 裏的類是不是和將要反序列化後的對象對應的類是相同的版本。該指紋實現爲一個64bit的long 類型。通過安全的Hash算法(SHA-1)來將序列化的類的基本信息(包括類名稱、類的編輯者、類的父接口及各種屬性等信息)處理爲該64bit的指紋。我們可以通過JDK自帶的命令serialver來打印某個可序列化類的指信息。如下:
E:\Test\src>serialver serial.SerialClass
serial.SerialClass: static final long serialVersionUID = -5764322004903657926L; 

問題的出現 :如果經過多次修改,會得到不同指紋信息,當一個指紋信息已變化的序列化對象在另一虛擬機反序列化時,由於另一虛擬機上類的指紋信息與反序列化對象的指紋信息不同,所以在反序列會過程中會出現異常。下面我們來做一個實驗: 
1、比如有這樣一個類:

Java代碼  收藏代碼
  1. public class SerialClass implements Serializable {  
  2.     public String firstName;  
  3. }  

2、現在我們把它序列化到一個文件中,代碼如下:

Java代碼  收藏代碼
  1. public void testSerial() {  
  2.     try {  
  3.         SerialClass sc = new SerialClass();  
  4.         sc.firstName = "j";  
  5.         ObjectOutputStream oos = new ObjectOutputStream(  
  6.                 new FileOutputStream("serail.dat"));  
  7.         oos.writeObject(sc);  
  8.         oos.close();  
  9.     } catch (FileNotFoundException e) {  
  10.         e.printStackTrace();  
  11.     } catch (IOException e) {  
  12.         e.printStackTrace();  
  13.     }  
  14. }  

3、現在我們來修改SerialClass,增加一個字段:

Java代碼  收藏代碼
  1. public class SerialClass implements Serializable {  
  2.     public String firstName;  
  3.     public String lastName;  
  4. }  

4、現在我們來反序列化,使用如下代碼:

Java代碼  收藏代碼
  1. public void testDeSerial() {  
  2.     ObjectInputStream ois;  
  3.     try {  
  4.         ois = new ObjectInputStream(new FileInputStream("serail.dat"));  
  5.         SerialClass sc = (SerialClass) ois.readObject();  
  6.         System.out.println(sc.firstName);  
  7.         ois.close();  
  8.     } catch (FileNotFoundException e) {  
  9.         e.printStackTrace();  
  10.     } catch (IOException e) {  
  11.         e.printStackTrace();  
  12.     } catch (ClassNotFoundException e) {  
  13.         e.printStackTrace();  
  14.     }  
  15. }  

5、運行時拋如下異常: 
java.io.InvalidClassException: serial.SerialClass; local class incompatible: stream classdesc serialVersionUID = -7303985972226829323, local class serialVersionUID = -5764322004903657926


從上面異常可以看出是由於類被更新,導致指紋信息發生變化反序列化時出錯 

解決辦法 :在需要序列化SerialClass類加上 private static final long serialVersionUID 版本屬性,並且值爲以修改前的指紋值-7303985972226829323L,這樣在編譯時就不會自動要所代碼來生成指紋信息了,而是做我們指定的指紋。修改後代碼如下:

Java代碼  收藏代碼
  1. public class SerialClass implements Serializable {  
  2.     private static final long serialVersionUID = -7303985972226829323L;  
  3.     public String firstName;  
  4.     public String lastName;  
  5. }  

現在我們再來反一把,結果正常。

結論 :在我們實現Serializable接口時一定要指定版本信息屬性 serialVersionUID ,這樣在我們修改類後,指紋信息不會發生改變,使用修改過的類反序列化時兼容對以前創建的序列化對象。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章