對象的序列化用途:
  Java 序列化技術可以使你將一個對象的狀態寫入一個Byte 流里,并且可以從其它地方
把該Byte 流里的數據讀出來。重新構造一個相同的對象。這種機制允許你將對象通過網絡
進行傳播,并可以隨時把對象持久化到數據庫、文件等系統里。Java的序列化機制是RMI、
EJB、JNNI等技術的技術基礎。




序列化的特點:
(1)如果某個類能夠被序列化,其子類也可以被序列化。
(2)聲明為static和transient類型的成員數據不能被序列化。因為static代表類的狀態,transient代表對象的臨時數據。
(3)相關的類和接口:在java.io包中提供如下涉及對象的序列化的類與接口ObjectOutput接口、ObjectOutputStream類、ObjectInput接口、ObjectInputStream類


private void writeObject(java.io.ObjectOutputStream out)
???? throws IOException
private void readObject(java.io.ObjectInputStream in)
???? throws IOException, ClassNotFoundException;






如何正確的使用Java序列化技術- -

[轉]http://blog.csdn.net/yethyeth/archive/2006/05/21/747933.aspx

http://publishblog.blogchina.com/blog/tb.b?diaryID=279864???????????????????????????????????????

?

摘要:本文比較全面的介紹了Java 序列化技術方方面面的知識,從序列化技術的基礎談起,
介紹了Java 序列化技術的機制和序列化技術的原理。并在隨后的部分詳細探討了序列化的
高級主題-如何精確的控制序列化機制。通過閱讀該文章,你可以了解如何使用Java 序列
化機制的方式和正確使用的方法,避免實際編程中對該技術的誤用。并能掌握如何高效使用
該技術來完成特殊的功能。

關鍵字:序列化(Serialize)、反序列化(DeSerialize)、類加載(ClassLoad)、指紋技術
(fingerprint)


1 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系統的介紹。
參考資料:
1、 SUN關于Java 的虛擬機規范《The Java Virtual Machine Specification》;
2、 《Thinking in Java》;
3、 《基于Java平臺的組件化開發技術》。

?