我們知道帶有final標識的屬性是不變量,也就是說只能賦值一次,不能重複賦值,但是在序列化類中就有點複雜了,比如有這樣一個類:
public class Person implements Serializable{
private static final long serialVersionUID=71282334L;
//不變量
public fnal String name="混世魔王";
}
這個Person類(此時V1.0版本)被序列化,然後存儲在磁盤上,在反序列化時name屬性會重新計算其值(這與static變量不同,static變量壓根就沒有保存到數據流中),比如name屬性修改成了「德天使」(版本升級為V2.0),那麼反序列化對象的name值就是「德天使」。保持新舊對象的final變量相同,有利於代碼業務邏輯統一,這是序列化的基本規則之一,也就是說,如果final屬性是一個直接量,在反序列化時就會重新計算。對這基本規則不多說,我們要說的是final變量另外一種賦值方式:通過構造函數賦值。代碼如下:
public class Person implements Serializable{
private static final long serialVersionUID=91282334L;
//不變量初始不賦值
public final String name;
//構造函數為不變量賦值
public Person(){
name="混世魔王";
}
}
這也是我們常用的一種賦值方式,可以把這個Person類定義為版本V1.0,然後進行序列化,看看有什麼問題沒有,序列化的代碼如下所示:
public class Serialize{
public static void main(Stringargs){
//序列化以持久保存
SerializationUtils.writeObject(new Person());
}
}
Person的實例對像保存到了磁盤上,它是一個貧血對像(承載業務屬性定義,但不包含其行為定義),我們做一個簡單的模擬,修改一下name值代表變更,要注意的是serialVersionUID保持不變,修改後的代碼如下:
public class Person implements Serializable{
private static final long serialVersionUID=91282334L;
//不變量初始不賦值
public final String name;
//構造函數為不變量賦值
public Person(){
name="德天使";
}
}
此時Person類的版本是V2.0,但serialVersionUID沒有改變,仍然可以反序列化,其代碼如下:
public class Deserialize{
public static void main(Stringargs){
//反序列化
Person p=(Person)SerializationUtils.readObject();
System.out.println(p.name);
}
}
現在問題來了:打印的結果是什麼?是混世魔王還是德天使?
答案即將揭曉,答案是:混世魔王。
final類型的變量不是會重新計算嗎?答案應該是「德天使」才對啊,為什麼會是「混世魔王」?這是因為這裡觸及了反序列化的另一個規則:反序列化時構造函數不會執行。
反序列化的執行過程是這樣的:JVM從數據流中獲取一個Object對象,然後根據數據流中的類文件描述信息(在序列化時,保存到磁盤的對象文件中包含了類描述信息,注意是類描述信息,不是類)查看,發現是final變量,需要重新計算,於是引用Person類中的name值,而此時JVM又發現name竟然沒有賦值,不能引用,於是它很「聰明」地不再初始化,保持原值狀態,所以結果就是「混世魔王」了。
讀者不要以為這樣的情況很少發生,如果使用Java開發過桌面應用,特別是參與過對性能要求較高的項目(比如交易類項目),那麼很容易遇到這樣的問題。比如一個C/S結構的在線外匯交易系統,要求提供24小時的聯機服務,如果在升級的類中有一個final變量是構造函數賦值的,而且新舊版本還發生了變化,則在應用請求熱切的過程中(非常短暫,可能只有30秒),很可能就會出現反序列化生成的final變量值與新產生的實例值不相同的情況,於是業務異常就產生了,情況嚴重的話甚至會影響交易數據,那可是天大的事故了。
注意 在序列化類中,不使用構造函數為final變量賦值。