一、概念
把對象轉換爲字節序列的過程稱爲對象的序列化。
把字節序列恢復爲對象的過程稱爲對象的反序列化。
對象的序列化主要有兩種用途:
1) 把對象的字節序列永久地保存到硬盤上,通常存放在一個文件中;
2) 在網絡上傳送對象的字節序列。
二、JDK類庫中的序列化API
java.io.ObjectOutputStream代表對象輸出流,它的writeObject(Object obj)方法可對參數指定的obj對象進行序列化,把得到的字節序列寫到一個目標輸出流中。
java.io.ObjectInputStream代表對象輸入流,它的readObject()方法從一個源輸入流中讀取字節序列,再把它們反序列化爲一個對象,並將其返回。
1.只有實現了Serializable和Externalizable接口的類的對象才能被序列化。
Externalizable接口繼承自 Serializable接口,實現Externalizable接口的類完全由自身來控制序列化的行爲,而僅實現Serializable接口的類可以 採用默認的序列化方式 。
例子:
package com.sid.io;
import java.io.Serializable;
public class TestModel implements Serializable {
private static final long serialVersionUID= -1L;
private String name;
private int age;
public static long getSerialVersionUID() {
return serialVersionUID;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
package com.sid.io;
import java.io.*;
public class SerializableMain {
public static void main(String[] args) throws Exception {
serializable();
deserializable();
}
public static void serializable() throws Exception{
TestModel testModel = new TestModel();
testModel.setAge(22);
testModel.setName("sid");
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(new File("Serializable.txt")));
oos.writeObject(testModel);
oos.close();
}
public static void deserializable() throws Exception{
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File("Serializable.txt")));
TestModel testModel =(TestModel) ois.readObject();
System.out.println(testModel.getAge());
System.out.println(testModel.getName());
ois.close();
}
}
結果
2.transient關鍵字修飾
不想序列化某些字段,則該字段用transient關鍵字修飾
比如:把上訴TestModel類的name字段用transient修飾
再跑一邊SerializableMain結果:
父類的序列化與 Transient 關鍵字
情境:一個子類實現了 Serializable 接口,它的父類都沒有實現 Serializable 接口,序列化該子類對象,然後反序列化後輸出父類定義的某變量的數值,該變量數值與序列化時的數值不同。
解決:要想將父類對象也序列化,就需要讓父類也實現Serializable 接口。如果父類不實現的話的,就 需要有默認的無參的構造函數。在父類沒有實現 Serializable 接口時,虛擬機是不會序列化父對象的,而一個 Java 對象的構造必須先有父對象,纔有子對象,反序列化也不例外。所以反序列化時,爲了構造父對象,只能調用父類的無參構造函數作爲默認的父對象。因此當我們取父對象的變量值時,它的值是調用父類無參構造函數後的值。如果你考慮到這種序列化的情況,在父類無參構造函數中對變量進行初始化,否則的話,父類變量值都是默認聲明的值,如 int 型的默認是 0,string 型的默認是 null。
Transient 關鍵字的作用是控制變量的序列化,在變量聲明前加上該關鍵字,可以阻止該變量被序列化到文件中,在被反序列化後,transient 變量的值被設爲初始值,如 int 型的是 0,對象型的是 null。
特性使用案例
我們熟悉使用 Transient 關鍵字可以使得字段不被序列化,那麼還有別的方法嗎?根據父類對象序列化的規則,我們可以將不需要被序列化的字段抽取出來放到父類中,子類實現 Serializable 接口,父類不實現,根據父類序列化規則,父類的字段數據將不被序列化,形成類圖如圖 2 所示。
圖 2. 案例程序類圖
上圖中可以看出,attr1、attr2、attr3、attr5 都不會被序列化,放在父類中的好處在於當有另外一個 Child 類時,attr1、attr2、attr3 依然不會被序列化,不用重複抒寫 transient,代碼簡潔。
3.serialVersionUID有什麼用
注意看TestModel類裏面有一個serialVersionUID
字面意思上是序列化的版本號,凡是實現Serializable接口的類都有一個表示序列化版本標識符的靜態變量
虛擬機是否允許反序列化,不僅取決於類路徑和功能代碼是否一致,一個非常重要的一點是兩個類的序列化 ID 是否一致(就是 private static final long serialVersionUID = 1L)。雖然兩個類的功能代碼、package、類名完全一致,但是序列化 ID 不同,他們無法相互序列化和反序列化。
由於實現了Serializable接口,如果沒有顯示在類中定義靜態final的serialVersionUID, Java會自動給這個class進行一個摘要算法,類似於指紋算法,只要這個文件 多一個空格,得到的UID就會截然不同的。一旦SerialversionUID 跟之前不匹配,反序列化就無法成功。
所以我們需要自己定義serialVersionUID,序列化的那一方的該類用的serialVersionUID要與反序列化的那一方的該類用的serialVersionUID一樣
顯式地定義serialVersionUID有兩種用途:
1、 在某些場合,希望類的不同版本對序列化兼容,因此需要確保類的不同版本具有相同的serialVersionUID;
2、 在某些場合,不希望類的不同版本對序列化兼容,因此需要確保類的不同版本具有不同的serialVersionUID。
4.靜態變量序列化問題
public class Test implements Serializable {
private static final long serialVersionUID = 1L;
public static int staticVar = 5;
public static void main(String[] args) {
try {
//初始時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();
}
}
}
將對象序列化後,修改靜態變量的數值,再將序列化對象讀取出來,然後通過讀取出來的對象獲得靜態變量的數值並打印出來。這個 System.out.println(t.staticVar) 語句輸出的是 10 。
原因在於序列化時,並不保存靜態變量,序列化保存的是對象的狀態,靜態變量屬於類的狀態,因此 序列化並不保存靜態變量。
5.對敏感字段加密
情境:服務器端給客戶端發送序列化對象數據,對象中有一些數據是敏感的,比如密碼字符串等,希望對該密碼字段在序列化時,進行加密,而客戶端如果擁有解密的密鑰,只有在客戶端進行反序列化時,纔可以對密碼進行讀取,這樣可以一定程度保證序列化對象的數據安全。
解決:在序列化過程中,虛擬機會試圖調用對象類裏的 writeObject 和 readObject 方法,進行用戶自定義的序列化和反序列化,如果沒有這樣的方法,則默認調用是 ObjectOutputStream 的 defaultWriteObject 方法以及 ObjectInputStream 的 defaultReadObject 方法。用戶自定義的 writeObject 和 readObject 方法可以允許用戶控制序列化的過程,比如可以在序列化的過程中動態改變序列化的數值。基於這個原理,可以在實際應用中得到使用,用於敏感字段的加密工作,清單 3 展示了這個過程。
清單 3. 靜態變量序列化問題代碼
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
|
在清單 3 的 writeObject 方法中,對密碼進行了加密,在 readObject 中則對 password 進行解密,只有擁有密鑰的客戶端,纔可以正確的解析出密碼,確保了數據的安全。執行清單 3 後控制檯輸出如圖 3 所示。
圖 3. 數據加密演示
特性使用案例
RMI 技術是完全基於 Java 序列化技術的,服務器端接口調用所需要的參數對象來至於客戶端,它們通過網絡相互傳輸。這就涉及 RMI 的安全傳輸的問題。一些敏感的字段,如用戶名密碼(用戶登錄時需要對密碼進行傳輸),我們希望對其進行加密,這時,就可以採用本節介紹的方法在客戶端對密碼進行加密,服務器端進行解密,確保數據傳輸的安全性。