JAVA序列化技術概述

Java 序列化技術可以使你將一個對象的狀態寫入一個Byte 流裏,並且可以從其它地方
把該Byte 流裏的數據讀出來。重新構造一個相同的對象。這種機制允許你將對象通過網絡
進行傳播,並可以隨時把對象持久化到數據庫、文件等系統裏。Java的序列化機制是RMI、
EJB、JNNI等技術的技術基礎。
1.1 序列化技術基礎
並非所有的Java 類都可以序列化,爲了使你指定的類可以實現序列化,你必須使該類
實現如下接口:
java.io.Serializable
需要注意的是,該接口什麼方法也沒有。實現該類只是簡單的標記你的類準備支持序列
化功能。我們來看如下的代碼:
/**
* 抽象基本類,完成一些基本的定義
*/
public abstract class Humanoid
{
protected int noOfHeads;
private static int totalHeads;
public Humanoid()
{
this(1);
}
public Humanoid(int noOfHeads)
{
如何正確的使用Java序列化技術 技術研究系列
if (noOfHeads > 10)
throw new Error("Be serious. More than 10 heads?!");
this.noOfHeads = noOfHeads;
synchronized (Humanoid.class)
{
totalHeads += noOfHeads;
}
}
public int getHeadCount()
{
return totalHeads;
}
}
該類的一個子類如下:
/**
* Humanoid的實現類,實現了序列化接口
*/
import java.io.*;
public class Person extends Humanoid
implements java.io.Serializable
{
private String lastName;
private String firstName;
private transient Thread workerThread;
private static int population;
public Person(String lastName, String firstName)
{
this.lastName = lastName;
this.firstName = firstName;
synchronized (Person.class)
{
population++;
}
}
public String toString()
{
return "Person " + firstName + " " + lastName;
}
static synchronized public int getPopulation()
{
return population;
}
}
1.2 對象的序列化及反序列化
上面的類Person 類實現了Serializable 接口,因此是可以序列化的。我們如果要把一個
可以序列化的對象序列化到文件裏或者數據庫裏,需要下面的類的支持:
java.io.ObjectOutputStream
如何正確的使用Java序列化技術 技術研究系列
下面的代碼負責完成Person類的序列化操作:
/**
* Person的序列化類,通過該類把Person寫入文件系統裏。
*/
import java.io.*;
public class WriteInstance
{
public static void main(String [] args) throws Exception
{
if (args.length != 1)
{
System.out.println("usage: java WriteInstance file");
System.exit(-1);
}
FileOutputStream fos = new FileOutputStream(args[0]);
ObjectOutputStream oos = new ObjectOutputStream(fos);
Person p = new Person("gaoyanbing", "haiger");
oos.writeObject(p);
}
}
如果我們要序列化的類其實是不能序列化的,則對其進行序列化時會拋出下面的異常:
java.io.NotSerializableException
當我們把Person 序列化到一個文件裏以後,如果需要從文件中恢復Person 這個對象,
我們需要藉助如下的類:
java.io.ObjectInputStream
從文件裏把Person類反序列化的代碼實現如下:
/**
* Person的反序列化類,通過該類從文件系統中讀出序列化的數據,並構造一個
* Person對象。
*/
import java.io.*;
public class ReadInstance
{
public static void main(String [] args) throws Exception
{
if (args.length != 1)
{
System.out.println("usage: java ReadInstance filename");
System.exit(-1);
}
FileInputStream fis = new FileInputStream(args[0]);
ObjectInputStream ois = new ObjectInputStream(fis);
Object o = ois.readObject();
如何正確的使用Java序列化技術 技術研究系列
System.out.println("read object " + o);
}
}
1.3 序列化對類的處理原則
並不是一個實現了序列化接口的類的所有字段及屬性都是可以序列化的。我們分爲以下
幾個部分來說明:
u 如果該類有父類,則分兩種情況來考慮,如果該父類已經實現了可序列化接口。則
其父類的相應字段及屬性的處理和該類相同;如果該類的父類沒有實現可序列化接
口,則該類的父類所有的字段屬性將不會序列化。
u 如果該類的某個屬性標識爲static類型的,則該屬性不能序列化;
u 如果該類的某個屬性採用transient關鍵字標識,則該屬性不能序列化;
需要注意的是,在我們標註一個類可以序列化的時候,其以下屬性應該設置爲transient
來避免序列化:
u 線程相關的屬性;
u 需要訪問IO、本地資源、網絡資源等的屬性;
u 沒有實現可序列化接口的屬性;(注:如果一個屬性沒有實現可序列化,而我們又
沒有將其用transient 標識, 則在對象序列化的時候, 會拋出
java.io.NotSerializableException 異常)。
1.4 構造函數和序列化
對於父類的處理,如果父類沒有實現序列化接口,則其必須有默認的構造函數(即沒有
參數的構造函數)。爲什麼要這樣規定呢?我們來看實際的例子。仍然採用上面的Humanoid
和Person 類。我們在其構造函數裏分別加上輸出語句:
/**
* 抽象基本類,完成一些基本的定義
*/
public abstract class Humanoid
{
protected int noOfHeads;
private static int totalHeads;
public Humanoid()
{
this(1);
System.out.println("Human's default constructor is invoked");
}
public Humanoid(int noOfHeads)
{
if (noOfHeads > 10)
throw new Error("Be serious. More than 10 heads?!");
如何正確的使用Java序列化技術 技術研究系列
this.noOfHeads = noOfHeads;
synchronized (Humanoid.class)
{
totalHeads += noOfHeads;
}
}
public int getHeadCount()
{
return totalHeads;
}
}
/**
* Humanoid的實現類,實現了序列化接口
*/
import java.io.*;
public class Person extends Humanoid
implements java.io.Serializable
{
private String lastName;
private String firstName;
private transient Thread workerThread;
private static int population;
public Person(String lastName, String firstName)
{
this.lastName = lastName;
this.firstName = firstName;
synchronized (Person.class)
{
population++;
}
System.out.println("Person's constructor is invoked");
}
public String toString()
{
return "Person " + firstName + " " + lastName;
}
static synchronized public int getPopulation()
{
return population;
}
}
在命令行運行其序列化程序和反序列化程序的結果爲:
如何正確的使用Java序列化技術 技術研究系列
可以看到,在從流中讀出數據構造Person對象的時候,Person 的父類Humanoid的默認
構造函數被調用了。當然,這點完全不用擔心,如果你沒有給父類一個默認構造函數,則編
譯的時候就會報錯。
這裏,我們把父類Humanoid做如下的修改:
/**
* 抽象基本類,完成一些基本的定義
*/
public class Humanoid implements java.io.Serializable
{
protected int noOfHeads;
private static int totalHeads;
public Humanoid()
{
this(1);
System.out.println("Human's default constructor is invoked");
}
public Humanoid(int noOfHeads)
{
if (noOfHeads > 10)
throw new Error("Be serious. More than 10 heads?!");
this.noOfHeads = noOfHeads;
synchronized (Humanoid.class)
{
totalHeads += noOfHeads;
}
}
public int getHeadCount()
{
return totalHeads;
}
}
我們把父類標記爲可以序列化, 再來看運行的結果:
如何正確的使用Java序列化技術 技術研究系列
可以看到,在反序列化的時候,如果父類也是可序列化的話,則其默認構造函數也不會
調用。這是爲什麼呢?
這是因爲Java 對序列化的對象進行反序列化的時候,直接從流裏獲取其對象數據來生
成一個對象實例,而不是通過其構造函數來完成,畢竟我們的可序列化的類可能有多個構造
函數,如果我們的可序列化的類沒有默認的構造函數,反序列化機制並不知道要調用哪個構
造函數纔是正確的。
1.5 序列化帶來的問題
我們可以看到上面的例子,在Person 類裏,其字段population 很明顯是想跟蹤在一個
JVM裏Person類有多少實例,這個字段在其構造函數裏完成賦值,當我們在同一個JVM 裏
序列化Person 並反序列化時,因爲反序列化的時候Person 的構造函數並沒有被調用,所以
這種機制並不能保證正確獲取Person在一個JVM的實例個數,在後面的部分我們將要詳細
探討這個問題及給出比較好的解決方案。
2 控制序列化技術
2.1 使用readObject 和writeObject方法
由於我們對於對象的序列化是採用如下的類來實現具體的序列化過程:
java.io.ObjectOutputStream
而該類主要是通過其writeObject 方法來實現對象的序列化過程,改類同時也提供了一
種機制來實現用戶自定義writeObject 的功能。方法就是在我們的需要序列化的類裏實現一
如何正確的使用Java序列化技術 技術研究系列
個writeObject方法,這個方法在ObjectOutputStream序列化該對象的時候就會自動的回調它。
從而完成我們自定義的序列化功能。
同樣的,反序列化的類也實現了同樣的回調機制,我們通過擴展其readObject來實現自
定義的反序列化機制。
通過這種靈活的回調機制就解決了上面提出的序列化帶來的問題,針對上面的Person
的問題,我們編寫如下的readObject方法就可以徹底避免population計數不準確的問題:
private void readObject(ObjectInputStream ois)
throws IOException, ClassNotFoundException
{
ois.defaultReadObject();
synchronized (Person.class)
{
population++;
}
System.out.println("Adjusting population in readObject");
}
2.2 序列化過程的類版本控制
本節討論以下問題:
u 在對象反序列化過程中如何尋找對象的類;
u 如果序列化和反序列化兩邊的類不是同一個版本,如何控制;
2.2.1 序列化類的尋找機制
在對象的反序列化過程中,是一定需要被反序列化的類能被ClassLoader 找到的,否則
在反序列化過程中就會拋出java.lang.ClassNotFoundException 異常。關於ClassLoader 如何
尋找類,這裏就不多說了,可以參考我的另一篇討論ClassLoader 的文章《在非管理環境下
如何實現熱部署》。我們這裏只是關心該序列化對象對應的類是被哪個ClassLoader 給Load
的。爲此,我們修改上面的
/**
* 修改後的反序列化類
*/
import java.io.*;
public class ReadInstance
{
public void readPerson(String filename)
{
如何正確的使用Java序列化技術 技術研究系列
try{
FileInputStream fis = new FileInputStream(filename);
ObjectInputStream ois = new ObjectInputStream(fis);
Object o = ois.readObject();
System.out.println("read object " + o);
System.out.println(this.getClass().getClassLoader());
Person person = (Person)o;
System.out.println(person.getClass().getClassLoader());
}catch(java.io.IOException ie)
{
ie.printStackTrace();
}catch(ClassNotFoundException ce)
{
ce.printStackTrace();
}
}
public static void main(String [] args) throws Exception
{
if (args.length != 1)
{
System.out.println("usage: java ReadInstance filename");
System.exit(-1);
}
ReadInstance readInstance = new ReadInstance();
readInstance.readPerson(args[0]);
}
我們主要通過背景爲黃色的兩行代碼查看其類加載器,運行結果如下:
由此可以看出,序列化類的類加載器正式其反序列化實現類的類加載器。這樣的話我們
就可以通過使最新的Person 類的版本發佈爲只有該反序列化器的ClassLoader可見。而較舊
的版本則不爲該ClassLoader 可見的方法來避免在反序列化過程中類的多重版本的問題。當
然,下面就類的版本問題我們還要做專門的探討。
如何正確的使用Java序列化技術 技術研究系列
2.2.2 序列化類多重版本的控制
如果在反序列化的JVM 裏出現了該類的不同時期的版本,那麼反序列化機制是如何處
理的呢?
爲了避免這種問題,Java的序列化機制提供了一種指紋技術,不同的類帶有不同版本的
指紋信息,通過其指紋就可以辨別出當前JVM 裏的類是不是和將要反序列化後的對象對應
的類是相同的版本。該指紋實現爲一個64bit的long 類型。通過安全的Hash算法(SHA-1)
來將序列化的類的基本信息(包括類名稱、類的編輯者、類的父接口及各種屬性等信息)處
理爲該64bit的指紋。我們可以通過JDK自帶的命令serialver來打印某個可序列化類的指紋
信息。如下:
當我們的兩邊的類版本不一致的時候,反序列化就會報錯:
如何正確的使用Java序列化技術 技術研究系列
解決之道:從上面的輸出可以看出,該指紋是通過如下的內部變量來提供的:
private static final long serialVersionUID;
如果我們在類裏提供對該屬性的控制,就可以實現對類的序列化指紋的自定義控制。爲
此,我們在Person 類裏定義該變量:
private static final long serialVersionUID= 6921661392987334380L;
則當我們修改了Person 類,發佈不同的版本到反序列化端的JVM,也不會有版本衝突
的問題了。需要注意的是,serialVersionUID 的值是需要通過serialver 命令來取得。而不能
自己隨便設置,否則可能有重合的。
需要注意的是,手動設置serialVersionUID 有時候會帶來一些問題,比如我們可能對類
做了關鍵性的更改。引起兩邊類的版本產生實質性的不兼容。爲了避免這種失敗,我們需要
知道什麼樣的更改會引起實質性的不兼容,下面的表格列出了會引起實質性不兼容和可以忽
略(兼容)的更改:
更改類型 例子
兼容的更改
u 添加屬性(Adding fields)
u 添加/刪除類(adding/removing classes)
u 添加/刪除writeObject/readObject方法(adding/removing
writeObject/readObject)
u 添加序列化標誌(adding Serializable)
u 改變訪問修改者(changing access modifier)
u 刪除靜態/不可序列化屬性(removing static/transient from
a field)
不兼容的更改
u 刪除屬性(Deleting fields)
u 在一個繼承或者實現層次裏刪除類(removing classes in a
hierarchy)
u 添加靜態/不可序列化字段(adding static/transient to a
field)
u 修改簡單變量類型(changing type of a primitive)
u switching between Serializable or Externalizable
u 刪除序列化標誌(removing Serializable/Externalizable)
u 改變readObject/writeObject對默認屬性值的控制(changing
whether readObject/writeObject handles default field
data)
u adding writeReplace or readResolve that produces
objects incompatible with older versions
另外,從Java 的序列化規範裏並沒有指出當我們對類做了實質性的不兼容修改後反序
列化會有什麼後果。並不是所有的不兼容修改都會引起反序列化的失敗。比如,如果我們刪
除了一個屬性,則在反序列化的時候,反序列化機制只是簡單的將該屬性的數據丟棄。從
JDK 的參考裏,我們可以得到一些不兼容的修改引起的後果如下表:
如何正確的使用Java序列化技術 技術研究系列
不兼容的修改 引起的反序列化結果
刪除屬性
(Deleting a field) Silently ignored
在一個繼承或者實現層次裏刪除類
(Moving classes in inheritance
hierarchy)
Exception
添加靜態/不可序列化屬性
(Adding static/transient)
Silently ignored
修改基本屬性類型
(Changing primitive type)
Exception
改變對默認屬性值的使用
(Changing use of default field data)
Exception
在序列化和非序列化及內外部類之間切換
(Switching Serializable and
Externalizable)
Exception
刪除Serializable或者Externalizable標誌
(Removing Serializable or
Externalizable)
Exception
返回不兼容的類
(Returning incompatible class)
Depends on incompatibility
2.3 顯示的控制對屬性的序列化過程
在默認的Java 序列化機制裏,有關對象屬性到byte 流裏的屬性的對應映射關係都是自
動而透明的完成的。在序列化的時候,對象的屬性的名稱默認作爲byte 流裏的名稱。當該
對象反序列化的時候,就是根據byte 流裏的名稱來對應映射到新生成的對象的屬性裏去的。
舉個例子來說。在我們的一個Person對象序列化的時候,Person的一個屬性firstName就作
爲byte 流裏該屬性默認的名稱。當該Person 對象反序列化的時候,序列化機制就把從byte
流裏得到的firstName 的值賦值給新的Person 實例裏的名叫firstName的屬性。
Java的序列化機制提供了相關的鉤子函數給我們使用,通過這些鉤子函數我們可以精確
的控制上述的序列化及反序列化過程。ObjectInputStream的內部類GetField提供了對把屬性
數據從流中取出來的控制,而ObjectOutputStream的內部類PutField則提供了把屬性數據放
入流中的控制機制。就ObjectInputStream來講,我們需要在readObject方法裏來完成從流中
讀取相應的屬性數據。比如我們現在把Person 類的版本從下面的表一更新到表二:
/**
* 修改前的老版本Person類,爲了簡化,我們刪除了所有無關的代碼
*/
import java.io.*;
public class Person extends Humanoid
implements java.io.Serializable
{
private String lastName;
如何正確的使用Java序列化技術 技術研究系列
private String firstName;
private static final long serialVersionUID =6921661392987334380L;
private Person()
{
}
public Person(String lastName, String firstName)
{
this.lastName = lastName;
this.firstName = firstName;
}
public String toString()
{
return "Person " + firstName + " " + lastName;
}
}
修改後的Person爲:
/**
* 修改後的Person類,我們將firstName和lastName變成了fullName
*/
import java.io.*;
public class Person extends Humanoid
implements java.io.Serializable
{
private String fullName;
private static final long serialVersionUID =6921661392987334380L;
private Person()
{
}
public Person(String fullName)
{
this.lastName = fullName;
}
public String toString()
{
return "Person " + fullName;
}
}
爲此,我們需要編寫Person類的readObject方法如下:
private void readObject(ObjectInputStream ois)
throws IOException,ClassNotFoundException
{
ObjectInputStream.GetField gf = ois.readFields();
fullName = (String) gf.get("fullName", null);
if (fullName == null)
{
String lastName = (String) gf.get("lastName", null);
如何正確的使用Java序列化技術 技術研究系列
String firstName = (String) gf.get("firstName", null);
if ( (lastName == null) || (firstName == null))
{
throw new InvalidClassException("invalid Person");
}
fullName = firstName + " " + lastName;
}
}
我們的執行順序是:
1) 編譯老的Person及所有類;
2) 將老的Person序列化到文件裏;
3) 修改爲新版本的Person類;
4) 編譯新的Person類;
5) 反序列化Person;
執行結果非常順利,修改後的反序列化機制仍然正確的從流中獲取了舊版本Person 的
屬性信息並完成對新版本的Person的屬性賦值。
使用ObjectInputStream的readObject 來處理反序列化的屬性時,有兩點需要注意:
u 一旦採用自己控制屬性的反序列化,則必須完成所有屬性的反序列化(即要給所有
屬性賦值);
u 在使用內部類GetField 的get 方法的時候需要注意,如果get 的是一個既不在老版
本出現的屬性,有沒有在新版本出現的屬性,則該方法會拋出異常:
IllegalArgumentException: no such field,所以我們應該在一個try塊裏
來使用該方法。
同理,我們可以通過writeObject 方法來控制對象屬性的序列化過程。這裏就不再一一
舉例了,如果你有興趣的話,可以自己實現Person 類的writeObject 方法,並且使用
ObjectOutputStream的內部類PutField來完成屬性的手動序列化操作。
3 總結
Java 序列化機制提供了強大的處理能力。一般來講,爲了儘量利用Java 提供的自動化
機制,我們不需要對序列化的過程做任何的干擾。但是在某些時候我們需要實現一些特殊的
功能,比如類的多版本的控制,特殊字段的序列化控制等。我們可以通過多種方式來實現這
些功能:
u 利用序列化機制提供的鉤子函數readObject和writeObject;
u 覆蓋序列化類的metaData 信息;
如何正確的使用Java序列化技術 技術研究系列
u 使類實現Externalizable 接口而不是實現Serializable接口。
關於Externalizable 接口更多的介紹,可以參考JDK 的幫助提供的詳細文檔,同時也可
以快速參考《Thinking in Java》這本書第十章-Java IO系統的介紹。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章