[改善Java代码]养成良好习惯,显式声明UID
建議11: 養成良好習慣,顯式聲明UID
我們編寫一個實現了Serializable接口(序列化標志接口)的類, Eclipse馬上就會給一個黃色警告:需要增加一個Serial Version ID。為什么要增加?它是怎么計算出來的?有什么用?本章就來解釋該問題。
類實現Serializable接口的目的是為了可持久化,比如網絡傳輸或本地存儲,為系統的分布和異構部署提供先決支持條件。若沒有序列化,現在我們熟悉的遠程調用、對象數據庫都不可能存在,我們來看一個簡單的序列化類:
1 public class Person implements Serializable{ 2 private String name; 3 4 public String getName() { 5 return name; 6 } 7 8 public void setName(String name) { 9 this.name = name; 10 } 11 12 }這是一個簡單JavaBean,實現了Serializable接口,可以在網絡上傳輸,也可以本地存儲然后讀取。這里我們以Java消息服務(Java Message Service)方式傳遞該對象(即通過網絡傳遞一個對象),定義在消息隊列中的數據類型為ObjectMessage,首先定義一個消息的生產者(Producer),代碼如下:
1 public class Producer { 2 public static void main(String[] args) throws Exception { 3 Person person = new Person(); 4 person.setName("混世魔王");6 //序列化,保存到磁盤上 7 SerializationUtils.writeObject(person); 8 } 9 }這里引入了一個工具類SerializationUtils,其作用是對一個類進行序列化和反序列化,并存儲到硬盤上(模擬網絡傳輸),其代碼如下:
1 import java.io.FileInputStream; 2 import java.io.FileOutputStream; 3 import java.io.ObjectInput; 4 import java.io.ObjectInputStream; 5 import java.io.ObjectOutputStream; 6 import java.io.Serializable; 7 8 public class SerializationUtils { 9 private static String FILE_NAME = "c:/obj.bin"; 10 // 序列化 11 public static void writeObject(Serializable s) { 12 try { 13 ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(FILE_NAME)); 14 oos.writeObject(s); 15 oos.close(); 16 } catch (Exception e) { 17 e.printStackTrace(); 18 } 19 } 20 21 public static Object readObject(){ 22 Object obj=null; 23 // 反序列化 24 try { 25 ObjectInput input = new ObjectInputStream(new FileInputStream(FILE_NAME)); 26 obj = input.readObject(); 27 input.close(); 28 } catch (Exception e) { 29 e.printStackTrace(); 30 } 31 return obj; 32 } 33 }通過對象序列化過程,把一個對象從內存塊轉化為可傳輸的數據流,然后通過網絡發送到消息消費者(Consumer)那里,并進行反序列化,生成實例對象,代碼如下:.
1 public class Consumer { 2 public static void main(String[] args) throws Exception { 3 // 反序列化 4 Person p = (Person) SerializationUtils.readObject(); 5 System.out.println("name="+p.getName()); 6 } 7 }這是一個反序列化過程,也就是對象數據流轉換為一個實例對象的過程,其運行后的輸出結果為:混世魔王。這太easy了,是的,這就是序列化和反序列化典型的demo。但此處隱藏著一個問題:如果消息的生產者和消息的消費者所參考的類(Person類)有差異,會出現何種神奇事件?比如:消息生產者中的Person類增加了一個年齡屬性,而消費者沒有增加該屬性。為啥沒有增加?!因為這是個分布式部署的應用,你甚至都不知道這個應用部署在何處,特別是通過廣播(broadcast)方式發送消息的情況,漏掉一兩個訂閱者也是很正常的。
這個時候給Person.java類增加一個age屬性.
1 import java.io.Serializable; 2 3 public class Person implements Serializable{ 4 private String name; 5 private int age; 6 7 public int getAge() { 8 return age; 9 } 10 11 public void setAge(int age) { 12 this.age = age; 13 } 14 15 public String getName() { 16 return name; 17 } 18 19 public void setName(String name) { 20 this.name = name; 21 } 22 }直接運行Consumer.java 會拋出?java.io.InvalidClassException異常
1 java.io.InvalidClassException: com.summerchill.staticproxy.Person; local class incompatible: stream classdesc serialVersionUID = 7107224374967840269, local class serialVersionUID = -6034120172421752969 2 at java.io.ObjectStreamClass.initNonProxy(Unknown Source) 3 at java.io.ObjectInputStream.readNonProxyDesc(Unknown Source) 4 at java.io.ObjectInputStream.readClassDesc(Unknown Source) 5 at java.io.ObjectInputStream.readOrdinaryObject(Unknown Source) 6 at java.io.ObjectInputStream.readObject0(Unknown Source) 7 at java.io.ObjectInputStream.readObject(Unknown Source) 8 at com.summerchill.staticproxy.SerializationUtils.readObject(SerializationUtils.java:28) 9 at com.summerchill.staticproxy.Consumer.main(Consumer.java:6) 10 Exception in thread "main" java.lang.NullPointerException 11 at com.summerchill.staticproxy.Consumer.main(Consumer.java:7)在這種序列化和反序列化的類不一致的情形下,反序列化時會報一個InvalidClassException異常,原因是序列化和反序列化所對應的類版本發生了變化,JVM不能把數據流轉換為實例對象。接著刨根問底:JVM是根據什么來判斷一個類版本的呢?
好問題,通過SerialVersionUID,也叫做流標識符(Stream Unique Identifier),即類的版本定義的,它可以顯式聲明也可以隱式聲明。顯式聲明格式如下:
1 private static final long serialVersionUID = XXXXXL;而隱式聲明則是我不聲明,你編譯器在編譯的時候幫我生成。生成的依據是通過包名、類名、繼承關系、非私有的方法和屬性,以及參數、返回值等諸多因子計算得出的,極度復雜,基本上計算出來的這個值是唯一的。
serialVersionUID如何生成已經說明了,我們再來看看serialVersionUID的作用。JVM在反序列化時,會比較數據流中的serialVersionUID與類的serialVersionUID是否相同,如果相同,則認為類沒有發生改變,可以把數據流load為實例對象;如果不相同,對不起,我JVM不干了,拋個異常InvalidClassException給你瞧瞧。這是一個非常好的校驗機制,可以保證一個對象即使在網絡或磁盤中“滾過”一次,仍能做到“出淤泥而不染”,完美地實現類的一致性。
但是,有時候我們需要一點特例場景,例如:我的類改變不大,JVM是否可以把我以前的對象反序列化過來?就是依靠顯式聲明serialVersionUID,向JVM撒謊說“我的類版本沒有變更”,如此,我們編寫的類就實現了向上兼容。我們修改一下上面的Person類,代碼如下:
1 public class Person implements Serializable{ 2 private static final long serialVersionUID = -6034120172421752969L; 3 private String name; 4 5 public String getName() { 6 return name; 7 } 8 9 public void setName(String name) { 10 this.name = name; 11 } 12 }Person.java類上加上了serialVersionUID之后再進行序列化的Producer.java和反序列化的Consumer.java之后
是不會拋出InvalidClassException異常的.
剛開始生產者和消費者持有的Person類版本一致,都是V1.0,某天生產者的Person類版本變更了,增加了一個“年齡”屬性,升級為V2.0,而由于種種原因(比如程序員疏忽、升級時間窗口不同等)消費端的Person還保持為V1.0版本,代碼如下:
1 public class Person implements Serializable{ 2 private static final long serialVersionUID = -6034120172421752969L; 3 private String name; 4 private int age; 5 public int getAge() { 6 return age; 7 } 8 9 public void setAge(int age) { 10 this.age = age; 11 } 12 13 public String getName() { 14 return name; 15 } 16 17 public void setName(String name) { 18 this.name = name; 19 } 20 }此時雖然生產者和消費者對應的類版本不同,但是顯式聲明的serialVersionUID相同,反序列化也是可以運行的,所帶來的業務問題就是消費端不能讀取到新增的業務屬性(age屬性)而已。
通過此例,我們的反序列化實現了版本向上兼容的功能,使用V1.0版本的應用訪問了一個V2.0版本的對象,這無疑提高了代碼的健壯性。我們在編寫序列化類代碼時,隨手加上serialVersionUID字段,也不會給我們帶來太多的工作量,但它卻可以在關鍵時候發揮異乎尋常的作用。
注意 顯式聲明serialVersionUID可以避免對象不一致,但盡量不要以這種方式向JVM“撒謊”。
?
總結
以上是生活随笔為你收集整理的[改善Java代码]养成良好习惯,显式声明UID的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 应该始终以PreparedStateme
- 下一篇: CentOS 7下的MariaDB Ma