十、序列化
1. 謹慎地使用Serializable接口
使一個類的實例可被序列化,只需要讓它實現Serializable接口即可。因爲簡單,程序員普遍對序列化存在誤解,以爲不需要做什麼工作就可以實現序列化,實際的情形卻複雜的多。
實現Serializable的代價有:
(1)一旦一個類被髮布,就大大降低了改變這個類的實現的靈活性
一個類實現了Serializable接口,它的字節流編碼就變成了導出API的一部分。一旦這個類被廣泛使用,就必須永遠支持這種序列化形式。如果不認真設計一種自定義的序列化形式,而僅僅接受默認的序列化形式,這種序列化形式將被永遠地束縛在該類最初的內部表示法上。
(2)它增加了出現bug和安全漏洞的可能性
通常是用構造器來構建對象的,序列化機制提供一種語言之外的對象創建機制,反序列化機制是一個隱藏的構造器,具備與其他構造器相同的特點。默認的反序列化機制很容易使對象的約束關係遭到破壞,也易遭受非法訪問。
(3)隨着類發行新的版本,相關的測試負擔也增加了
一旦發佈,需要確保各版本的兼容性
每當實現一個類的時候,都需要權衡一下實現序列化所付出的代價和帶來的好處。
內部類不應該實現Serializable。
靜態成員類可以實現Serializable。
如果一個類是爲了繼承而設計的,需要倍加留意子類序列化的需求。對於爲繼承而設計的不可序列化的類,應該考慮提供一個無參構造器,否則子類就無法進行序列化。
2. 考慮使用自定義的序列化形式
如果沒有認真考慮默認的序列化形式是否合適,就不要貿然接受。接受默認的序列化形式是一個非常重要的決定,需要從靈活性、性能和正確性多個角度對這種編碼形式進行考察。一般來講,只有當你自行設計的自定義序列化形式與默認的序列化形式基本相同時,才能接受默認的序列化形式。
public class Name implements Serializable {
private final String lastName;
private final String firstName;
private final String middleName;
...
}
即便是確定了默認的序列化形式是合適的,通常還必須提供一個readObject方法以保證約束關係和安全性。對於Name這個類而言,readObject方法必須保證lastName和firstName是非null的。
當一個對象的物理表示法與它的邏輯數據內容有實質性的區別時,使用默認序列化形式有以下4個缺點:
(1)它使這個類的導出API永遠地束縛在該類的內部表示法上。
序列化字節流也是導出API的一部分,也代表一種約定。
(2)它會消耗過多的空間。
例如鏈表的序列化,不僅表示鏈表中的每個項,還要表示所有的連接關係,這是不必要的。
(3)它會消耗過多的時間。
序列化邏輯並不瞭解對象圖的拓撲結構,所以它必須要經過一個昂貴的圖遍歷過程。
(4)它會引起棧溢出。
默認的序列化過程要對對象圖執行一次遞歸遍歷,即使對於中等規模的對象圖,這樣的操作也可能會引起棧溢出。
選擇錯誤的序列化形式對於一個類的複雜性和性能都會有永久的負面影響。
3. 保護性地編寫readObject方法
readObject相當於另一個公有的構造器,如同其他的構造器一樣,它也要求注意同樣的所有注意事項:必須檢查參數的有效性,必要的時候對參數進行保護性拷貝。如果readObject無法做到這兩點,就易遭受攻擊。
public class MutablePeriod {
// A period instance
public final Period period;
// period's start field, to which we shouldn't have access
public final Date start;
// period's end field, to which we shouldn't have access
public final Date end;
public MutablePeriod() {
try {
ByteArrayOutputStream bos =
new ByteArrayOutputStream();
ObjectOutputStream out =
new ObjectOutputStream(bos);
// Serialize a valid Period instance
out.writeObject(new Period(new Date(), new Date()));
/*
* Append rogue "previous object refs" for internal
* Date fields in Period. For details, see "Java
* Object Serialization Specification," Section 6.4.
*/
byte[] ref = { 0x71, 0, 0x7e, 0, 5 };
// Ref #5
bos.write(ref);
// The start field
ref[4] = 4;
// Ref # 4
bos.write(ref);
// The end field
// Deserialize Period and "stolen" Date references
ObjectInputStream in = new ObjectInputStream(
new ByteArrayInputStream(bos.toByteArray()));
period = (Period) in.readObject();
start = (Date) in.readObject();
end = (Date) in.readObject();
}
catch (IOException | ClassNotFoundException e) {
throw new AssertionError(e);
}
}
}
攻擊程序如下:
public static void main(String[] args) {
MutablePeriod mp = new MutablePeriod();
Period p = mp.period;
Date pEnd = mp.end;
// Let's turn back the clock
pEnd.setYear(78);
System.out.println(p);
// Bring back the 60s!
pEnd.setYear(69);
System.out.println(p);
}
readObject 方法可以確保 Period 類的約束條件不會遭到破壞,以保持它的不可變性:
// readObject method with defensive copying and validity checking
private void readObject(ObjectInputStream s)
throws IOException, ClassNotFoundException {
s.defaultReadObject();
// Defensively copy our mutable components
start = new Date(start.getTime());
end = new Date(end.getTime());
// Check that our invariants are satisfied
if (start.compareTo(end) > 0)
throw new InvalidObjectException(start +" after "+ end);
}
編寫健壯readObject方法的指導方針:
(1)對於對象引用域必須保持爲私有的類,要保護性地拷貝這些域中的每個對象。不可變類的可變組件就屬於這一類別。
(2)對於任何約束條件,如果檢查失敗,則拋出一個InvalidObjectException異常。這些檢查應該跟在所有的保護性拷貝之後。
(3)如果整個對象圖在被反序列化之後必須進行驗證,就應該使用ObjectInputValidation接口。
(4)無論是直接方式還是間接方式,都不要調用類中任何可覆蓋的方法。
4. 對於實例控制,枚舉類型優先於readResolve
如果單例類的聲明加上了“implements Serializable”的字樣,他就不再是一個單例。
public class Elvis {
public static final Elvis INSTANCE = new Elvis();
private Elvis() { ... }
public void leaveTheBuilding() { ... }
}
不管是採用默認的序列化方式,還是自定義的序列化方式,都相當於對外部提供了一個公有的構造器。對於可序列化的實例受控類(如可序列化的單例類),readResolve方法提供了一種控制實例構建的方法。
readResolve方法允許你用readObject創建的實例代替另一個實例。對於一個正在被反序列化的對象,如果它的類定義了一個readResolve方法,並且具備正確的聲明,那麼在反序列化之後,新建對象上的readResolve方法就會被調用,該方法返回的對象引用將被返回,取代新建的對象。在這個特性的絕大多數用法中,指向新建對象的引用不需要再被引用,立即成爲垃圾回收的對象。
如果Elvis類要實現Serializable接口,下面的readResolve方法就足以保證它的單例屬性:
// readResolve for instance control - you can do better!
private Object readResolve() {
// Return the one true Elvis and let the garbage collector
// take care of the Elvis impersonator.
return INSTANCE;
}
如果依賴readResolve進行實例控制,帶有對象引用的所有實例域都必須聲明爲transient的,否則就可能會被攻擊。
如有你將你的可序列化的實例受控(instance-controlled)類編寫成枚舉,Java就可以保證除了所聲明的常量之外,不會有別的實例。
// Enum singleton - the preferred approach
public enum Elvis {
INSTANCE;
private String[] favoriteSongs = { "Hound Dog", "Heartbreak Hotel" };
public void printFavorites() {
System.out.println(Arrays.toString(favoriteSongs));
}
}
應該儘可能地使用枚舉類型來實施實例控制的約束條件。如果做不到,同時又需要一個既可序列化又是實例受控的類,就必須提供一個readResolve方法,並確保該類的所有實例域都爲基本類型,或者是transient的。
5. 考慮用序列化代理代替序列化實例
實現Serializable接口,會增加出錯和出現安全問題的可能性,因爲它是利用語言之外的機制來創建對象的,而不是使用普通的構造器。序列化代理模式可以極大地減少上述風險的發生。
實現序列化代理模式的方法:
- 爲可序列化的類設計一個私有的靜態嵌套類,精確地表示外圍類實例的邏輯狀態。
- 將writeReplace方法添加到外圍類中。
- 在外圍類中添加如下readObject方法。
- 在代理類中提供一個readResolve方法,它返回一個邏輯上相當的外圍類實例。
public final class Period implements Serializable {
private final Date start;
private final Date end;
public Period(Date start, Date end) {
this.start = new Date(start.getTime());
this.end = new Date(end.getTime());
if (this.start.compareTo(this.end) > 0) {
throw new IllegalArgumentException(start + " after " + end);
}
}
public Date start() {
return new Date(start.getTime());
}
public Date end() {
return new Date(end.getTime());
}
private void readObject(ObjectInputStream in) throws InvalidObjectException {
throw new InvalidObjectException("Proxy required");
}
//將外圍類的實例轉變成了它的序列化代理
private Object writeReplace() {
return new SerializationProxy(this);
}
private static class SerializationProxy implements Serializable {
private final Date start;
private final Date end;
SerializationProxy(Period p) {
this.start = p.start;
this.end = p.end;
}
private Object readResolve() {
return new Period(start, end);
}
private static final long serialVersionUID = 234098243823485285L;
}
}