讀古今文學網 > 編寫高質量代碼:改善Java程序的151個建議 > 建議11:養成良好習慣,顯式聲明UID >

建議11:養成良好習慣,顯式聲明UID

我們編寫一個實現了Serializable接口(序列化標誌接口)的類,Eclipse馬上就會給一個黃色警告:需要增加一個Serial Version ID。為什麼要增加?它是怎麼計算出來的?有什麼用?本章就來解釋該問題。

類實現Serializable接口的目的是為了可持久化,比如網絡傳輸或本地存儲,為系統的分佈和異構部署提供先決支持條件。若沒有序列化,現在我們熟悉的遠程調用、對像數據庫都不可能存在,我們來看一個簡單的序列化類:


public class Person implements Serializable{

private String name;

/*name屬性的getter/setter方法省略*/

}


這是一個簡單JavaBean,實現了Serializable接口,可以在網絡上傳輸,也可以本地存儲然後讀取。這裡我們以Java消息服務(Java Message Service)方式傳遞該對像(即通過網絡傳遞一個對像),定義在消息隊列中的數據類型為ObjectMessage,首先定義一個消息的生產者(Producer),代碼如下:


public class Producer{

public static void main(Stringargs)throws Exception{

Person person=new Person();

person.setName(\"混世魔王\");

//序列化,保存到磁盤上

SerializationUtils.writeObject(person);

}

}


這裡引入了一個工具類SerializationUtils,其作用是對一個類進行序列化和反序列化,並存儲到硬盤上(模擬網絡傳輸),其代碼如下:


public class SerializationUtils{

private static String FILE_NAME=\"c:/obj.bin\";

//序列化

public static void writeObject(Serializable s){

try{

ObjectOutputStream oos=new ObjectOutputStream(new

FileOutputStream(FILE_NAME));

oos.writeObject(s);

oos.close();

}catch(Exception e){

e.printStackTrace();

}

}

public static Object readObject(){

Object obj=null;

//反序列化

try{

Object Input in put=new Object Input Stream(new

FileInputStream(FILE_NAME));

obj=input.readObject();

input.close();

}catch(Exception e){

e.printStackTrace();

}

return obj;

}

}


通過對像序列化過程,把一個對像從內存塊轉化為可傳輸的數據流,然後通過網絡發送到消息消費者(Consumer)那裡,並進行反序列化,生成實例對象,代碼如下:


public class Consumer{

public static void main(Stringargs)throws Exception{

//反序列化

Person p=(Person)SerializationUtils.readObject();

System.out.println(\"name=\"+p.getName());

}

}


這是一個反序列化過程,也就是對像數據流轉換為一個實例對象的過程,其運行後的輸出結果為:混世魔王。這太easy了,是的,這就是序列化和反序列化典型的demo。但此處隱藏著一個問題:如果消息的生產者和消息的消費者所參考的類(Person類)有差異,會出現何種神奇事件?比如:消息生產者中的Person類增加了一個年齡屬性,而消費者沒有增加該屬性。為啥沒有增加?!因為這是個分佈式部署的應用,你甚至都不知道這個應用部署在何處,特別是通過廣播(broadcast)方式發送消息的情況,漏掉一兩個訂閱者也是很正常的。

在這種序列化和反序列化的類不一致的情形下,反序列化時會報一個InvalidClassException異常,原因是序列化和反序列化所對應的類版本發生了變化,JVM不能把數據流轉換為實例對象。接著刨根問底:JVM是根據什麼來判斷一個類版本的呢?

好問題,通過SerialVersionUID,也叫做流標識符(Stream Unique Identifier),即類的版本定義的,它可以顯式聲明也可以隱式聲明。顯式聲明格式如下:


private static final long serialVersionUID=XXXXXL;


而隱式聲明則是我不聲明,你編譯器在編譯的時候幫我生成。生成的依據是通過包名、類名、繼承關係、非私有的方法和屬性,以及參數、返回值等諸多因子計算得出的,極度複雜,基本上計算出來的這個值是唯一的。

serialVersionUID如何生成已經說明了,我們再來看看serialVersionUID的作用。JVM在反序列化時,會比較數據流中的serialVersionUID與類的serialVersionUID是否相同,如果相同,則認為類沒有發生改變,可以把數據流load為實例對像;如果不相同,對不起,我JVM不幹了,拋個異常InvalidClassException給你瞧瞧。這是一個非常好的校驗機制,可以保證一個對像即使在網絡或磁盤中「滾過」一次,仍能做到「出淤泥而不染」,完美地實現類的一致性。

但是,有時候我們需要一點特例場景,例如:我的類改變不大,JVM是否可以把我以前的對象反序列化過來?就是依靠顯式聲明serialVersionUID,向JVM撒謊說「我的類版本沒有變更」,如此,我們編寫的類就實現了向上兼容。我們修改一下上面的Person類,代碼如下:


public class Person implements Serializable{

private static fnal long serialVersionUID=55799L;

/*其他保持不變*/

}


剛開始生產者和消費者持有的Person類版本一致,都是V1.0,某天生產者的Person類版本變更了,增加了一個「年齡」屬性,升級為V2.0,而由於種種原因(比如程序員疏忽、升級時間窗口不同等)消費端的Person還保持為V1.0版本,代碼如下:


public class Person implements Serializable{

private static final long serialVersionUID=5799L;

private int age;

/*age、name的getter/setter方法省略*/

}


此時雖然生產者和消費者對應的類版本不同,但是顯式聲明的serialVersionUID相同,反序列化也是可以運行的,所帶來的業務問題就是消費端不能讀取到新增的業務屬性(age屬性)而已。

通過此例,我們的反序列化實現了版本向上兼容的功能,使用V1.0版本的應用訪問了一個V2.0版本的對象,這無疑提高了代碼的健壯性。我們在編寫序列化類代碼時,隨手加上serialVersionUID字段,也不會給我們帶來太多的工作量,但它卻可以在關鍵時候發揮異乎尋常的作用。

注意 顯式聲明serialVersionUID可以避免對像不一致,但盡量不要以這種方式向JVM「撒謊」。