什麼是序列化
java中的序列化(serialization)機制能夠將一個執行個體對象的狀態資訊寫入到一個位元組流中,使其可以通過socket進行傳輸、或者持久化儲存到資料庫或檔案系統中;然後在需要的時候,可以根據位元組流中的資訊來重構一個相同的對象。序列化機制在java中有著廣泛的應用,EJB、 RMI等技術都是以此為基礎的。
正確使用序列化機制
一般而言,要使得一個類可以序列化,只需簡單實現java.io.Serializable介面即可。該介面是一個標記式介面,它本身不包含任何內容,實現了該介面則表示這個類準備支援序列化的功能。如下例定義了類Person,並聲明其可以序列化。
Java代碼
1. public class Person implements java.io.Serializable {}
public class Person implements java.io.Serializable {}
序列化機制是通過java.io.ObjectOutputStream類和java.io.ObjectInputStream類來實現的。在序列化(serialize)一個對象的時候,會先執行個體化一個ObjectOutputStream對象,然後調用其 writeObject()方法;在還原序列化(deserialize)的時候,則會執行個體化一個ObjectInputStream對象,然後調用其 readObject()方法。下例說明了這一過程。
Java代碼
1. public void serializeObject(){
2. String fileName = "ser.out";
3. FileOutputStream fos = new FileOutputStream(fileName);
4. ObjectOutputStream oos = new ObjectOutputStream(fos);
5. oos.writeObject(new Person());
6. oos.flush();
7. }
8.
9. public void deserializeObject(){
10. String fileName = "ser.out";
11. FileInputStream fos = new FileInputStream(fileName);
12. ObjectInputStream oos = new ObjectInputStream(fos);
13. Person p = oos.readObject();
14. }
上例中我們對一個Person對象定義了序列化和還原序列化的操作。但如果Person類是不能序列化的話,即對不能序列化的類進行序列化操作,則會拋出 java.io.NotSerializableException異常。
JVM中有一個預定義的序列化實現機制,即預設調用 ObjectOutputStream.defaultWriteObject() 和 ObjectInputStream.defaultReadObject() 來執行序列化操作。如果想自訂序列化的實現,則必須在聲明了可序列化的類中實現 writeObject()和readObject()方法。
幾種使用方式
一般在序列化一個類A的時候,有以下三種情況:
# [list=3] 類A沒有父類,自己實現了Serializable介面
# 類A有父類B,且父類實現了Serializable介面
# 類A有父類B,但父類沒有實現Serializable介面
[/list]
對於第一種情況,直接實現Serializable介面即可。
對於第二種情況,因為父類B已經實現了Serializable介面,故類A無需實現此介面;如果父類實現了writeObject()和readObject(),則使用此方法,否則直接使用預設的機制。
對於第三種情況,則必須在類A中顯示實現writeObject()和readObject()方法來處理父類B的狀態資訊;還有一點要特別注意,在父類B中一定要有一個無參的建構函式,這是因為在還原序列化的過程中並不會使用聲明為可序列化的類A的任何建構函式,而是會調用其沒有申明為可序列化的父類B的無參建構函式。
序列化機制的一些問題
# [list] 效能問題
為了序列化類別A一個執行個體對象,所需儲存的全部資訊如下:
1. 與此執行個體對象相關的全部類的中繼資料(metadata)資訊;因為繼承關係,類A的執行個體對象也是其任一父類的對象。因而,需要將整個繼承鏈上的每一個類的中繼資料資訊,按照從父到子的順序依次儲存起來。
2. 類A的描述資訊。此描述資訊中可能包含有如下這些資訊:類的版本ID(version ID)、表示是否自訂了序列化實現機制的標誌、可序列化的屬性的數目、每個屬性的名字和值、及其可序列化的父類的描述資訊。
3. 將執行個體對象作為其每一個超類的執行個體對象,並將這些資料資訊都儲存起來。
# 在RMI等遠程調用的應用中,每調用一個方法,都需要傳遞如此多的資訊量;久而久之,會對系統的效能照成很大的影響。 版本資訊
當用readObject()方法讀取一個序列化對象的byte流資訊時,會從中得到所有相關類的描述資訊以及樣本對象的狀態資料;然後將此描述資訊與其本地要構造的類的描述資訊進行比較,如果相同則會建立一個新的執行個體並恢複其狀態,否則會拋出異常。這就是序列化對象的版本檢測。JVM中預設的描述資訊是使用一個長整型的雜湊碼(hashcode)值來表示,這個值與類的各個方面的資訊有關,如類名、類修飾符、所實現的介面名、方法和建構函式的資訊、屬性的資訊等。因而,一個類作一些微小的變動都有可能導致不同的雜湊碼值。例如開始對一個執行個體對象進行了序列化,接著對類增加了一個方法,或者更改了某個屬性的名稱,當再想根據序列化資訊來重構以前那個對象的時候,此時兩個類的版本資訊已經不匹配,不可能再恢複此對象的狀態了。要解決這個問題,可能在類中顯示定義一個值,如下所示:
Java代碼
1. private static final long serialVersionUID = ALongValue;
這樣,序列化機制會使用這個值來作為類的版本標識符,從而可以解決不相容的問題。但是它卻引入了一個新的問題,即使一個類作了實質性的改變,如增加或刪除了一些可序列化的屬性,在這種機制下仍然會認為這兩個類是相等的。
[/list]
一種更好的選擇
作為實現Serializable介面的一種替代方案,實現java.io.Externalizable介面同樣可以標識一個類為可序列化。
Externalizable介面中定義了以下兩個方法:
Java代碼
1. public void readExternal(ObjectInput in);
2. public void writeExternal(ObjectOutput out);
這兩個方法的功能與 readObject()和writeObject()方法相同,任何實現了Externalizable介面的類都需要這實現兩個函數來定義其序列化機制。
使用Externalizable比使用Serializable有著效能上的提高。前者序列化一個對象,所需儲存的資訊比後者要小,對於後者所需儲存的第3個方面的資訊,前者不需要訪問每一個父類並使其儲存相關的狀態資訊,而只需簡單地調用類中實現的writeExternal()方法即可。