java Serializable和Externalizable序列化反序列化詳解--轉


一、什么是序列化?
 
“對象序列化”(Object Serialization)是 Java1.1就開始有的特性。 簡單地說,就是可以將一個對象(標志對象的類型)及其狀態轉換為字節碼,保存起來(可以保存在數據庫,內存,文件等),然后可以在適當的時候再將其狀態恢復(也就是反序列化)。serialization 不但可以在本機做,而且可以經由網絡操作。它自動屏蔽了操作系統的差異,字節順序等。比如,在 Windows 平台生成一個對象並序列化之,然后通過網絡傳到一台 Unix 機器上,然后可以在這台Unix機器上正確地重構(deserialization)這個對象。 不必關心數據在不同機器上如何表示,也不必關心字節的順序或者其他任何細節。
 
另外,還應明白以下幾點:
 
a. java.io.Serializable接口沒有任何方法屬性域,實現它的類只是從語義上表明自己是可以序列化的。
 
b. 在對一個 Serializable(可序列化)對象進行重新裝配的過程中,不會調用任何構建器(甚至默認構建器)。整個對象都是通過從 InputStream 中取得數據恢復的。
 
c. 如是要一個類是可序列化的,那么它的子類也是可序列化的。
 

二、序列化在什么時候用?
 
可提供對 Java 兩種主要特性的支持:
 
遠程方法調用(RMI):使本來存在於其他機器的對象可以表現出好象就在本地機器上的行為。將消息發給遠程對象時,需要通過對象序列化來傳輸參數和返回值。
 
對象的序列化也是 Java Beans 必需的。使用一個 Bean 時,它的狀態信息通常在設計期間配置好。程序啟動以后,這種狀態信息必須保存下來,以便程序啟動以后恢復;具體工作由對象序列化完成。
 
三   序列化過程
 
java.io.ObjectOutputStream代表對象輸出流,它的writeObject(Object obj)方法可對參數指定的obj對象進行序列化,把得到的字節序列寫到一個目標輸出流中。
 
  java.io.ObjectInputStream代表對象輸入流,它的readObject()方法從一個源輸入流中讀取字節序列,再把它們反序列化為一個對象,並將其返回。、
 
  只有實現了Serializable和Externalizable接口的類的對象才能被序列化。Externalizable接口繼承自Serializable接口,實現Externalizable接口的類完全由自身來控制序列化的行為,而僅實現Serializable接口的類可以采用默認的序列化方式 。
 
  對象序列化包括如下步驟:
 
  1) 創建一個對象輸出流,它可以包裝一個其他類型的目標輸出流,如文件輸出流;
 
  2) 通過對象輸出流的writeObject()方法寫對象。
 
  對象反序列化的步驟如下:
 
  1) 創建一個對象輸入流,它可以包裝一個其他類型的源輸入流,如文件輸入流;
 
  2) 通過對象輸入流的readObject()方法讀取對象。
 
  下面讓我們來看一個對應的例子,類的內容如下:

01.import java.io.*; 
02.import java.util.Date; 
03. 
04. 
09. 
10.public class ObjectSaver { 
11.  
15. 
16.public static void main(String[] args) throws Exception { 
17. ObjectOutputStream out = new ObjectOutputStream 
18.(new FileOutputStream("D:""objectFile.obj")); 
19. 
20. //序列化對象 
21. 
22. Customer customer = new Customer("阿蜜果", 24); 
23. out.writeObject("你好!"); 
24. out.writeObject(new Date()); 
25. out.writeObject(customer); 
26. out.writeInt(123); //寫入基本類型數據 
27. out.close(); 
28. //反序列化對象 
29. 
30. ObjectInputStream in = new ObjectInputStream 
31.(new FileInputStream("D:""objectFile.obj")); 
32. 
33. System.out.println("obj1=" + (String) in.readObject()); 
34. System.out.println("obj2=" + (Date) in.readObject()); 
35. Customer obj3 = (Customer) in.readObject(); 
36. System.out.println("obj3=" + obj3); 
37. int obj4 = in.readInt(); 
38. System.out.println("obj4=" + obj4); 
39. in.close(); 
40.} 
41.} 
42. 
43.class Customer implements Serializable { 
44.private String name; 
45.private int age; 
46.public Customer(String name, int age) { 
47.this.name = name; 
48.this.age = age; 
49.} 
50. 
51.public String toString() { 
52.return "name=" + name + ", age=" + age; 
53.} 
54.}  

 

 
輸出結果如下:
 
obj1=你好!
 
obj2=Sat Sep 15 22:02:21 CST 2007
 
obj3=name=阿蜜果, age=24
 
obj4=123

四、如何序列化
 
在Java里,如果要使一個類可以序列化或反序列化,只需要實現 java.io.Serializable 接口。如果類沒有實現這個接口,則一般來說不能將他們的狀態進行序列化與反序列化。注意,這里我說"一般來說",是因為Java還提供了另外一個接口 java.io.Externalizable,關於這個接口的使用我將會在下面單獨說明。
 
1. transient(臨時)關鍵字
 
控制序列化過程時,可能有一個特定的子對象不願讓Java的序列化機制自動保存與恢復。一般地,若那個子對象包含了不想序列化的敏感信息(如密碼),就會面臨這種情況。即使那種信息在對象中具有“private”(私有)屬性,但一旦經序列化處理,人們就可以通過讀取一個文件,或者攔截網絡傳輸得到它。
 
為防止對象的敏感部分被序列化,一個辦法是將自己的類實現為Externalizable,這樣一來,沒有任何東西可以自動序列化,只能在 writeExternal() 明確序列化那些需要的部分。
 
然而,若操作的是一個 Serializable 對象,所有序列化操作都會自動進行。為解決這個問題,可以用transient(臨時)逐個字段地關閉序列化。
 
五、特殊情況-父類不可序列化而子類可序列化
 
我們看到,對於B的屬性b1,其可以正確的序列化,但對於其從A繼承過來的屬性a,b則沒有正確的序列化。為什么呢?我們再看上面的運行結果,可以發現:反序列化的時候,由於B實現了Serializable,所在以反序列化的時候,它並不會調用它自己的構造器,但是,在反序列化B的時候,卻調用了它的超類的構造器(實際上不僅僅是構造器,A的所有的初始化過程都會正常進行)。這正是上面結果中a,b的值沒有正確反序列化的原因。
 
於是,對於這種父類非序列化而子類可序列化的類,子類應該自己對超類的public,protected,以及 friedly 屬性進行單獨處理。
 
六.可序列化類的不同版本的序列化兼容性
 
  凡是實現Serializable接口的類都有一個表示序列化版本標識符的靜態變量:
 
private static final long serialVersionUID;

  以上serialVersionUID的取值是Java運行時環境根據類的內部細節自動生成的。如果對類的源代碼作了修改,再重新編譯,新生成的類文件的serialVersionUID的取值有可能也會發生變化。
類的serialVersionUID的默認值完全依賴於Java編譯器的實現,對於同一個類,用不同的Java編譯器編譯,有可能會導致不同的serialVersionUID,也有可能相同。為了提高哦啊serialVersionUID的獨立性和確定性,強烈建議在一個可序列化類中顯示的定義serialVersionUID,為它賦予明確的值。顯式地定義serialVersionUID有兩種用途:
 
  1) 在某些場合,希望類的不同版本對序列化兼容,因此需要確保類的不同版本具有相同的serialVersionUID;
 
  2) 在某些場合,不希望類的不同版本對序列化兼容,因此需要確保類的不同版本具有不同的serialVersionUID。
 
七、關於 writeReplace()與readResolve()
 
對於實現 Serializable 或 Externalizable 接口的類來說,writeReplace() 方法可以使對象被寫入流以前,用一個對象來替換自己。當序列化時,可序列化的類要將對象寫入流,如果我們想要另一個對象來替換當前對象來寫入流,則可以要實現下面這個方法,方法的簽名也要完全一致:ANY-ACCESS-MODIFIER Object writeReplace() throws ObjectStreamException;
writeReplace()方法在 ObjectOutputStream 准備將對象寫入流以前調用, ObjectOutputStream 會首先檢查序列化的類是否定義了 writeReplace()方法,如果定義了這個方法,則會通過調用它,用另一個對象替換它寫入流中。方法返回的對象要么與它替換的對象類型相同,要么與其兼容,否則,會拋出 ClassCastException 。
 
同理,當反序列化時,要將一個對象從流中讀出來,我們如果想將讀出來的對象用另一個對象實例替換,則要實現跟下面的方法的簽名完全一致的方法。ANY-ACCESS-MODIFIER Object readResolve() throws ObjectStreamException;
 
readResolve 方法在對象從流中讀取出來的時候調用, ObjectInputStream 會檢查反序列化的對象是否已經定義了這個方法,如果定義了,則讀出來的對象返回一個替代對象。同 writeReplace()方法,返回的對象也必須是與它替換的對象兼容,否則拋出 ClassCastException。如果序列化的類中有這些方法,那么它們的執行順序是這樣的:
a. writeReplace()
b. writeObject()
c. readObject()
d. readResolve()
 
下面是 java doc 中關於 readResolve() 與 writeReplace()方法的英文描述:
 
Serializable classes that need to designate an alternative object to be used when writing an object to the stream should implement this special method with the exact signature:


 ANY-ACCESS-MODIFIER Object writeReplace() throws ObjectStreamException;
 This writeReplace method is invoked by serialization if the method exists and it would be accessible from a method defined within the class of the object being serialized. Thus, the method can have private, protected and package-private access. Subclass access to this method follows java accessibility rules.

Classes that need to designate a replacement when an instance of it is read from the stream should implement this special method with the exact signature.
 
 ANY-ACCESS-MODIFIER Object readResolve() throws ObjectStreamException;
 This readResolve method follows the same invocation rules and accessibility rules as writeReplace.
 
八. 

其實這個問題簡單思考一下就可以搞清楚,方法是不帶狀態的,就是一些指令,指令是不需要序列化的,只要你的JVM classloader可以load到這個類,那么類方法指令自然就可以獲得。序列化真正需要保存的只是對象屬性的值,和對象的類型。
 
這些知識找一本Java基礎編程的書,或者Java手冊就可以查到,我以為是不應該犯這種基本概念錯誤的。

我們可以做一個簡單的小試驗,來證實一下:

 

package com.javaeye; 
 
import java.io.Serializable; 
 
public class DomainObject  implements Serializable { 
 
    private String name; 
     
    private int age ; 
 
    public int getAge(); { 
        return age; 
    } 
 
    public void setAge(int age); { 
        this.age = age; 
    } 
 
    public String getName(); { 
        return name; 
    } 
 
    public void setName(String name); { 
        this.name = name; 
    } 
     
     
}  
  
package com.javaeye; 
 
import java.io.FileOutputStream; 
import java.io.ObjectOutputStream; 
 
public class Main { 
 
    public static void main(String[] args); throws Exception { 
        DomainObject obj = new DomainObject();; 
        obj.setAge(29);; 
        obj.setName("fankai");; 
        FileOutputStream fos = new FileOutputStream("DomainObject");; 
        ObjectOutputStream oos = new ObjectOutputStream(fos);; 
        oos.writeObject(obj);; 
        oos.close();; 
        fos.close();; 
    } 
 
} 


DomainObject是我們准備序列化的類,在Main里面,我們new一個DomainObject的對象,然后賦值,最后把該對象序列化到一個硬盤文件中。

然后使用一種支持二進制編輯器,例如UltraEdit打開這個文件,看看Java都對DomainObject序列化了哪些信息,你就什么都明白了。

為了更方便觀察,我使用Linux下面的strings去提取文本信息,輸出為:

robbin@linux:~> strings DomainObject
com.javaeye.DomainObject
ageL
namet
Ljava/lang/String;xp
fankai

這些信息很直觀的告訴我們序列化都保存了些什么內容:
1)對象的類型
2)對象屬性的類型
3)對象屬性的值

並沒有什么方法簽名的信息,更不要說什么序列化方法了。

然后我們再做一個試驗,給DomainObject增加兩個方法:

 

01.package com.javaeye;    
02.   
03.import java.io.Serializable;    
04.   
05.public class DomainObject  implements Serializable {    
06.   
07.    public String toString(); {    
08.        return "This is a serializable test!";    
09.    }    
10.        
11.    public void doSomeWork(); {    
12.        System.out.println("hello");;    
13.    }    
14.}   
29. 
30. 
31.修改Main類如下:  
32. 
33.package com.javaeye;    
34.   
35.import java.io.FileOutputStream;    
36.import java.io.ObjectOutputStream;    
37.   
38.public class Main {    
39.   
40.    public static void main(String[] args); throws Exception {    
41.        DomainObject obj = new DomainObject();;    
42.   
43.        FileOutputStream fos = new FileOutputStream("DomainObject");;    
44.        ObjectOutputStream oos = new ObjectOutputStream(fos);;    
45.        oos.writeObject(obj);;    
46.        oos.close();;    
47.        fos.close();;    
48.    }    
49.   
50.}   
51.

 

我們增加了toString方法和doSomeWork方法,按照你的理論,如果序列化方法的話,產生的文件體積必然增大。記錄一下文件體積,92Byte,好了,刪除,運行程序,生成了新的文件,看一下體積,還是92Byte!
 
拿到Linux下面再提取一下字符串:

robbin@linux:~> strings DomainObject
com.javaeye.DomainObject
ageL
namet
Ljava/lang/String;xp
fankai

完全一模一樣!

然后我們再做第三個試驗,這次把DomainObject的兩個屬性以及相關方法刪除掉.

01.package com.javaeye;    
02.   
03.import java.io.Serializable;    
04.   
05.public class DomainObject  implements Serializable {    
06.    
14.}   
29. 
30. 
31.修改Main類如下:  
32. 
33.package com.javaeye;    
34.   
35.import java.io.FileOutputStream;    
36.import java.io.ObjectOutputStream;    
37.   
38.public class Main {    
39.   
40.    public static void main(String[] args); throws Exception {    
41.        DomainObject obj = new DomainObject();;    
42.   
43.        FileOutputStream fos = new FileOutputStream("DomainObject");;    
44.        ObjectOutputStream oos = new ObjectOutputStream(fos);;    
45.        oos.writeObject(obj);;    
46.        oos.close();;    
47.        fos.close();;    
48.    }    
49.   
50.}   
51.

 

按照你的理論,如果序列化方法的話,我們必然應該在文件里面發現方法的簽名信息,甚至方法里面包含的字符串,好了,再運行一遍,然后打開看一下吧!文件現在體積變成了45Byte,拿到Linux下面提取一下信息:
 
robbin@linux:~> strings DomainObject
com.javaeye.DomainObject

只有對象的類型信息,再無其它東西了!

請記住序列化機制只保存對象的類型信息,屬性的類型信息和屬性值,和方法沒有什么關系,你就是給這個類增加10000個方法,序列化內容也不會增加任何東西,不要想當然的臆測自己不了解的知識,動手去做!
 
序列化在 Effective Java 中講得很清楚啊, 一般認為只聲明實現 implements 接口, 不提供自定義的序列化形式是不負責任的做法, 這樣可能導致比較多的問題比如類的不同版本之間的兼容性, 看看 Effective Java 中的條目吧

    謹慎地實現 Serialiable
   
    為了繼承而設計的類應該很少實現 Serialiable, 接口也應該很少會擴展它. 如果違反了這條規則, 則擴展這個類或者實現這個接口的程序員會背上沉重的負擔.
   
     若沒有認真考慮默認序列化形式是否合適, 則不要接受這種形式
  
    即使你確定了默認序列化形式是合適的, 通常你仍然要提供一個 readObject方法以保證約束關系和約束性
  
    不管你選擇了哪種序列化形式, 你都要為自己編寫的每個可序列化的類聲明一個顯式的序列版本 UID (serialVersionUID)

原文地址:http://blog.sina.com.cn/s/blog_4e345ce70100rt86.html


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM