1. 文章結(jié)構(gòu)
- 序列化 ID 的問題
- 靜態(tài)變量序列化
- 父類的序列化與 Transient 關(guān)鍵字
- 對(duì)敏感字段加密
- 序列化存儲(chǔ)規(guī)則
2. 序列化 ID 問題
簡(jiǎn)單來說宝与,Java的序列化機(jī)制是通過在運(yùn)行時(shí)判斷類的serialVersionUID來驗(yàn)證版本一致性的浦箱。在進(jìn)行反序列化時(shí),JVM會(huì)把傳來的字節(jié)流中的serialVersionUID與本地相應(yīng)實(shí)體(類)的serialVersionUID進(jìn)行比較脱篙,如果相同就認(rèn)為是一致的,可以進(jìn)行反序列化,否則就會(huì)出現(xiàn)序列化版本不一致的異常惫搏。
當(dāng)實(shí)現(xiàn)java.io.Serializable接口的實(shí)體(類)沒有顯式地定義一個(gè)名為serialVersionUID谓晌,類型為long的變量時(shí)掠拳,Java序列化機(jī)制會(huì)根據(jù)編譯的class自動(dòng)生成一個(gè)serialVersionUID作序列化版本比較用,這種情況下纸肉,只有同一次編譯生成的class才會(huì)生成相同的serialVersionUID 溺欧。
如果我們不希望通過編譯來強(qiáng)制劃分軟件版本,即實(shí)現(xiàn)序列化接口的實(shí)體能夠兼容先前版本柏肪,未作更改的類姐刁,就需要顯式地定義一個(gè)名為serialVersionUID,類型為long的變量烦味,不修改這個(gè)變量值的序列化實(shí)體都可以相互進(jìn)行串行化和反串行化聂使。
package com.inout;
import java.io.Serializable;
public class A implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
package com.inout;
import java.io.Serializable;
public class A implements Serializable {
private static final long serialVersionUID = 2L;
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
3. 靜態(tài)變量序列化
看代碼
public class Test implements Serializable {
private static final long serialVersionUID = 1L;
public static int staticVar = 5;
public static void main(String[] args) {
try {
//初始時(shí)staticVar為5
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("result.obj"));
out.writeObject(new Test());
out.close();
//序列化后修改為10
Test.staticVar = 10;
ObjectInputStream oin = new ObjectInputStream(new FileInputStream("result.obj"));
Test t = (Test) oin.readObject();
oin.close();
//再讀取,通過t.staticVar打印新的值
System.out.println(t.staticVar);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
清單 2 中的 main 方法谬俄,將對(duì)象序列化后岩遗,修改靜態(tài)變量的數(shù)值,再將序列化對(duì)象讀取出來凤瘦,然后通過讀取出來的對(duì)象獲得靜態(tài)變量的數(shù)值并打印出來宿礁。依照清單 2,這個(gè) System.out.println(t.staticVar) 語句輸出的是 10 還是 5 呢蔬芥?
最后的輸出是 10梆靖,對(duì)于無法理解的讀者認(rèn)為,打印的 staticVar 是從讀取的對(duì)象里獲得的笔诵,應(yīng)該是保存時(shí)的狀態(tài)才對(duì)返吻。之所以打印 10 的原因在于序列化時(shí),并不保存靜態(tài)變量乎婿,這其實(shí)比較容易理解测僵,序列化保存的是對(duì)象的狀態(tài),靜態(tài)變量屬于類的狀態(tài),因此 序列化并不保存靜態(tài)變量捍靠。
4. 父類的序列化與 Transient 關(guān)鍵字
情境:一個(gè)子類實(shí)現(xiàn)了 Serializable 接口沐旨,它的父類都沒有實(shí)現(xiàn) Serializable 接口,序列化該子類對(duì)象榨婆,然后反序列化后輸出父類定義的某變量的數(shù)值磁携,該變量數(shù)值與序列化時(shí)的數(shù)值不同。
解決:要想將父類對(duì)象也序列化良风,就需要讓父類也實(shí)現(xiàn)Serializable 接口谊迄。如果父類不實(shí)現(xiàn)的話的,就 需要有默認(rèn)的無參的構(gòu)造函數(shù)烟央。在父類沒有實(shí)現(xiàn) Serializable 接口時(shí)统诺,虛擬機(jī)是不會(huì)序列化父對(duì)象的,而一個(gè) Java 對(duì)象的構(gòu)造必須先有父對(duì)象疑俭,才有子對(duì)象粮呢,反序列化也不例外。所以反序列化時(shí)怠硼,為了構(gòu)造父對(duì)象鬼贱,只能調(diào)用父類的無參構(gòu)造函數(shù)作為默認(rèn)的父對(duì)象。因此當(dāng)我們?nèi)「笇?duì)象的變量值時(shí)香璃,它的值是調(diào)用父類無參構(gòu)造函數(shù)后的值这难。如果你考慮到這種序列化的情況,在父類無參構(gòu)造函數(shù)中對(duì)變量進(jìn)行初始化葡秒,否則的話姻乓,父類變量值都是默認(rèn)聲明的值,如 int 型的默認(rèn)是 0眯牧,string 型的默認(rèn)是 null蹋岩。
Transient 關(guān)鍵字的作用是控制變量的序列化,在變量聲明前加上該關(guān)鍵字学少,可以阻止該變量被序列化到文件中剪个,在被反序列化后,transient 變量的值被設(shè)為初始值版确,如 int 型的是 0扣囊,對(duì)象型的是 null。
代碼范例:
class TestFather {
String name;
}
class TestChild extends TestFather implements Serializable {
private static final long serialVersionUID = -5085120126097209576L;
int age = 30;
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public void setFatherName(String name) {
super.name = name;
}
public String getFatherName() {
return super.name;
}
}
public static void main(String[] args) throws Exception {
TestChild c = new TestChild();
c.setFatherName("abc");
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("test1.obj"));
oos.writeObject(c);
oos.close();
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("test1.obj"));
TestChild cc = (TestChild) ois.readObject();
System.out.println(cc.getFatherName());
}
結(jié)果輸出
null
特性使用案例
我們熟悉使用 Transient 關(guān)鍵字可以使得字段不被序列化绒疗,那么還有別的方法嗎侵歇?根據(jù)父類對(duì)象序列化的規(guī)則,我們可以將不需要被序列化的字段抽取出來放到父類中吓蘑,子類實(shí)現(xiàn) Serializable 接口惕虑,父類不實(shí)現(xiàn),根據(jù)父類序列化規(guī)則,父類的字段數(shù)據(jù)將不被序列化溃蔫,形成類圖如圖 2 所示健提。
5. 對(duì)敏感字段加密
情境:服務(wù)器端給客戶端發(fā)送序列化對(duì)象數(shù)據(jù),對(duì)象中有一些數(shù)據(jù)是敏感的酒唉,比如密碼字符串等矩桂,希望對(duì)該密碼字段在序列化時(shí)沸移,進(jìn)行加密痪伦,而客戶端如果擁有解密的密鑰,只有在客戶端進(jìn)行反序列化時(shí)雹锣,才可以對(duì)密碼進(jìn)行讀取网沾,這樣可以一定程度保證序列化對(duì)象的數(shù)據(jù)安全。
解決:在序列化過程中蕊爵,虛擬機(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)用中得到使用咧最,用于敏感字段的加密工作捂人,清單 3 展示了這個(gè)過程。
class PasswordObj implements Serializable{
private static final long serialVersionUID = 3847029983123556071L;
private String password = "password";
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
//序列化時(shí)對(duì)密碼加密
private void writeObject(ObjectOutputStream stream) throws IOException {
System.out.println("原始密碼: " + password);
password = "encryption";
System.out.println("加密后的密碼: " + password);
stream.writeObject(password);
}
//反序列化時(shí)解密
private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException {
String s = (String) stream.readObject();
System.out.println("讀取到的密碼: " + s);
password = "password";
}
}
6. 序列化存儲(chǔ)規(guī)則
問題代碼如下:
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("result.obj"));
Test test = new Test();
//試圖將對(duì)象兩次寫入文件
out.writeObject(test);
out.flush();
System.out.println(new File("result.obj").length());
out.writeObject(test);
out.close();
System.out.println(new File("result.obj").length());
ObjectInputStream oin = new ObjectInputStream(new FileInputStream("result.obj"));
//從文件依次讀出兩個(gè)文件
Test t1 = (Test) oin.readObject();
Test t2 = (Test) oin.readObject();
oin.close();
//判斷兩個(gè)引用是否指向同一個(gè)對(duì)象
System.out.println(t1 == t2);
對(duì)同一對(duì)象兩次寫入文件矢沿,打印出寫入一次對(duì)象后的存儲(chǔ)大小和寫入兩次后的存儲(chǔ)大小滥搭,然后從文件中反序列化出兩個(gè)對(duì)象,比較這兩個(gè)對(duì)象是否為同一對(duì)象捣鲸。一般的思維是瑟匆,兩次寫入對(duì)象,文件大小會(huì)變?yōu)閮杀兜拇笮≡曰蹋葱蛄谢瘯r(shí)愁溜,由于從文件讀取,生成了兩個(gè)對(duì)象媒役,判斷相等時(shí)應(yīng)該是輸入 false 才對(duì)祝谚,但是最后結(jié)果是 true。
我們看到酣衷,第二次寫入對(duì)象時(shí)文件只增加了 5 字節(jié)交惯,并且兩個(gè)對(duì)象是相等的,這是為什么呢?
解答:Java 序列化機(jī)制為了節(jié)省磁盤空間席爽,具有特定的存儲(chǔ)規(guī)則意荤,當(dāng)寫入文件的為同一對(duì)象時(shí),并不會(huì)再將對(duì)象的內(nèi)容進(jìn)行存儲(chǔ)只锻,而只是再次存儲(chǔ)一份引用玖像,上面增加的 5 字節(jié)的存儲(chǔ)空間就是新增引用和一些控制信息的空間。反序列化時(shí)齐饮,恢復(fù)引用關(guān)系捐寥,使得清單 3 中的 t1 和 t2 指向唯一的對(duì)象,二者相等祖驱,輸出 true握恳。該存儲(chǔ)規(guī)則極大的節(jié)省了存儲(chǔ)空間。
特性案例分析
查看清單 5 的代碼捺僻。
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);
清單 4 的目的是希望將 test 對(duì)象兩次保存到 result.obj 文件中乡洼,寫入一次以后修改對(duì)象屬性值再次保存第二次,然后從 result.obj 中再依次讀出兩個(gè)對(duì)象匕坯,輸出這兩個(gè)對(duì)象的 i 屬性值束昵。案例代碼的目的原本是希望一次性傳輸對(duì)象修改前后的狀態(tài)。
結(jié)果兩個(gè)輸出的都是 1葛峻,原因就是第一次寫入對(duì)象以后锹雏,第二次再試圖寫的時(shí)候,虛擬機(jī)根據(jù)引用關(guān)系知道已經(jīng)有一個(gè)相同對(duì)象已經(jīng)寫入文件泞歉,因此只保存第二次寫的引用逼侦,所以讀取時(shí),都是第一次保存的對(duì)象腰耙。讀者在使用一個(gè)文件多次 writeObject 需要特別注意這個(gè)問題榛丢。