什么是序列化?
序列化是將對(duì)象存儲(chǔ)為二進(jìn)制格式。
在序列化的過程中,對(duì)象和它的元數(shù)據(jù)(比如對(duì)象的類名和它的屬性名稱)存儲(chǔ)為二進(jìn)制格式。并且在需要的時(shí)候可以恢復(fù)對(duì)象(反序列化)。
對(duì)象序列化的兩個(gè)使用場景:
- 對(duì)象持久化:將對(duì)象的狀態(tài)持久化,比如存儲(chǔ)到數(shù)據(jù)庫中。
- 對(duì)象遠(yuǎn)程傳輸:將對(duì)象從一臺(tái)計(jì)算機(jī)發(fā)送到另外一臺(tái)計(jì)算機(jī)。
實(shí)現(xiàn)序列化
如果希望對(duì)象能夠序列化需要實(shí)現(xiàn)一個(gè)接口,即序列化接口(Java.io.Serializable )。
import java.io.Serializable;
public class Person implements Serializable {
}
通過調(diào)用ObjectOutputStream.writeObject(Object o) 來實(shí)現(xiàn)將對(duì)象持久化
之后通過ObjectInputStream.readObject() 實(shí)現(xiàn)反序列化。
父類序列化與Transient關(guān)鍵字
情景:一個(gè)子類實(shí)現(xiàn)了Serializable接口,它的父類沒有實(shí)現(xiàn)Serializable接口序列化該對(duì)子類對(duì)象,然后反序列化輸出父類某域的數(shù)值,該變量與序列化時(shí)的數(shù)值不同(為0或?yàn)閚ull)。
解決:要想父類對(duì)象也序列化,就需要讓父類也實(shí)現(xiàn)Serializable接口。如果父類不實(shí)現(xiàn)的話就需要有默認(rèn)的無參構(gòu)造函數(shù),在父類沒有實(shí)現(xiàn)Serializable接口時(shí),虛擬機(jī)是不會(huì)序列化父對(duì)象的,而一個(gè)Java對(duì)象的實(shí)例化過程必先伴隨著父類對(duì)象的實(shí)例化,實(shí)例化也不例外。只能調(diào)用父類對(duì)象的無參構(gòu)造函數(shù)作為父類實(shí)例化。
Transient 關(guān)鍵字的作用是控制變量的序列化,在變量聲明前加上該關(guān)鍵字,可以阻止該變量被序列化到文件中,在被反序列化后,transient 變量的值被設(shè)為初始值,如 int 型的是 0,對(duì)象型的是 null。
我們熟悉使用 Transient 關(guān)鍵字可以使得字段不被序列化,那么還有別的方法嗎?根據(jù)父類對(duì)象序列化的規(guī)則,我們可以將不需要被序列化的字段抽取出來放到父類中,子類實(shí)現(xiàn) Serializable 接口,父類不實(shí)現(xiàn),根據(jù)父類序列化規(guī)則,父類的字段數(shù)據(jù)將不被序列化。
通過以上我們大致可以分析得出序列化和反序列化的實(shí)現(xiàn)是區(qū)別與構(gòu)造函數(shù)的,在反序列化的過程中并不會(huì)去顯式調(diào)用已經(jīng)序列化對(duì)象的構(gòu)造函數(shù),而是當(dāng)其父類沒有實(shí)現(xiàn)序列化接口時(shí)候再去調(diào)用無參構(gòu)造函數(shù),通過這樣的機(jī)制可以包裝那些不需要序列化的域,所有語言的設(shè)計(jì)都是有其深層含義的 。這和Java語言設(shè)計(jì)的品味有關(guān)。
當(dāng)然當(dāng)一個(gè)父類對(duì)象實(shí)現(xiàn)序列化之后其子類默認(rèn)實(shí)現(xiàn)序列化。
序列化的存儲(chǔ)規(guī)則
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("result.obj"));
Test test = new Test();
test.i = 1;
out.writeObject(test);
out.flush();
test.i = 2;
out.writeObject(test);
out.close();
ObjectInputStream oin = new ObjectInputStream(new FileInputStream(
"result.obj"));
Test t1 = (Test) oin.readObject();
Test t2 = (Test) oin.readObject();
System.out.println(t1.i);
System.out.println(t2.i);
按照我們目前的理解輸出結(jié)果應(yīng)該分別為1和2,但事實(shí)上輸出的結(jié)果都是1,Java 序列化機(jī)制為了節(jié)省磁盤空間,具有特定的存儲(chǔ)規(guī)則,當(dāng)寫入文件的為同一對(duì)象時(shí),并不會(huì)再將對(duì)象的內(nèi)容進(jìn)行存儲(chǔ),而只是再次存儲(chǔ)一份引用,而當(dāng)反序列化時(shí),恢復(fù)引用關(guān)系,使得代碼中的 t1 和 t2 指向唯一的對(duì)象,第一次寫入對(duì)象以后,第二次再試圖寫的時(shí)候,虛擬機(jī)根據(jù)引用關(guān)系知道已經(jīng)有一個(gè)相同對(duì)象已經(jīng)寫入文件,因此只保存第二次寫的引用,所以讀取時(shí),都是第一次保存的對(duì)象。因此寫入相同對(duì)象時(shí)候需要注意這個(gè)問題。
序列化處理敏感字段
在序列化過程中,虛擬機(jī)會(huì)試圖調(diào)用對(duì)象類里的 writeObject 和 readObject 方法,進(jìn)行用戶自定義的序列化和反序列化,如果沒有這樣的方法,則默認(rèn)調(diào)用是 ObjectOutputStream 的 defaultWriteObject 方法以及 ObjectInputStream 的 defaultReadObject 方法。用戶自定義的 writeObject 和 readObject 方法可以允許用戶控制序列化的過程,比如可以在序列化的過程中動(dòng)態(tài)改變序列化的數(shù)值。基于這個(gè)原理,可以在實(shí)際應(yīng)用中得到使用,用于敏感字段的加密工作。
public class Person
implements java.io.Serializable
{
public Person(String fn, String ln, int a)
{
this.firstName = fn; this.lastName = ln; this.age = a;
}
public String getFirstName() { return firstName; }
public String getLastName() { return lastName; }
public int getAge() { return age; }
public Person getSpouse() { return spouse; }
public void setFirstName(String value) { firstName = value; }
public void setLastName(String value) { lastName = value; }
public void setAge(int value) { age = value; }
public void setSpouse(Person value) { spouse = value; }
private void writeObject(java.io.ObjectOutputStream stream)
throws java.io.IOException
{
//簡單加密數(shù)據(jù)
age = age << 2;
stream.defaultWriteObject();
}
private void readObject(java.io.ObjectInputStream stream)
throws java.io.IOException, ClassNotFoundException
{
stream.defaultReadObject();
// 數(shù)據(jù)的解碼
age = age << 2;
}
public String toString()
{
return "[Person: firstName=" + firstName +
" lastName=" + lastName +
" age=" + age +
" spouse=" + (spouse!=null ? spouse.getFirstName() : "[null]") +
"]";
}
private String firstName;
private String lastName;
private int age;
private Person spouse;
}
序列化 ID 問題
虛擬機(jī)是否允許反序列化,不僅取決于類路徑和功能代碼是否一致,一個(gè)非常重要的一點(diǎn)是兩個(gè)類的序列化 ID 是否一致。即使雖然兩個(gè)類的功能代碼完全一致,但是序列化 ID 不同,他們無法相互序列化和反序列化。序列化 ID 在 Eclipse 下提供了兩種生成策略,一個(gè)是固定的 1L,一個(gè)是隨機(jī)生成一個(gè)不重復(fù)的 long 類型數(shù)據(jù)(實(shí)際上是使用 JDK 工具生成),在這里有一個(gè)建議,如果沒有特殊需求,就是用默認(rèn)的 1L 就可以,這樣可以確保代碼一致時(shí)反序列化成功。那么隨機(jī)生成的序列化 ID 有什么作用呢,有些時(shí)候,通過改變序列化 ID 可以用來限制某些用戶的使用。
private static final long serialVersionUID = 1L;