[改善Java代码]养成良好习惯,显式声明UID
建議11: 養(yǎng)成良好習(xí)慣,顯式聲明UID
我們編寫一個(gè)實(shí)現(xiàn)了Serializable接口(序列化標(biāo)志接口)的類, Eclipse馬上就會(huì)給一個(gè)黃色警告:需要增加一個(gè)Serial Version ID。為什么要增加?它是怎么計(jì)算出來(lái)的?有什么用?本章就來(lái)解釋該問(wèn)題。
類實(shí)現(xiàn)Serializable接口的目的是為了可持久化,比如網(wǎng)絡(luò)傳輸或本地存儲(chǔ),為系統(tǒng)的分布和異構(gòu)部署提供先決支持條件。若沒有序列化,現(xiàn)在我們熟悉的遠(yuǎn)程調(diào)用、對(duì)象數(shù)據(jù)庫(kù)都不可能存在,我們來(lái)看一個(gè)簡(jiǎn)單的序列化類:
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 }這是一個(gè)簡(jiǎn)單JavaBean,實(shí)現(xiàn)了Serializable接口,可以在網(wǎng)絡(luò)上傳輸,也可以本地存儲(chǔ)然后讀取。這里我們以Java消息服務(wù)(Java Message Service)方式傳遞該對(duì)象(即通過(guò)網(wǎng)絡(luò)傳遞一個(gè)對(duì)象),定義在消息隊(duì)列中的數(shù)據(jù)類型為ObjectMessage,首先定義一個(gè)消息的生產(chǎn)者(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 }這里引入了一個(gè)工具類SerializationUtils,其作用是對(duì)一個(gè)類進(jìn)行序列化和反序列化,并存儲(chǔ)到硬盤上(模擬網(wǎng)絡(luò)傳輸),其代碼如下:
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 }通過(guò)對(duì)象序列化過(guò)程,把一個(gè)對(duì)象從內(nèi)存塊轉(zhuǎn)化為可傳輸?shù)臄?shù)據(jù)流,然后通過(guò)網(wǎng)絡(luò)發(fā)送到消息消費(fèi)者(Consumer)那里,并進(jìn)行反序列化,生成實(shí)例對(duì)象,代碼如下:.
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 }這是一個(gè)反序列化過(guò)程,也就是對(duì)象數(shù)據(jù)流轉(zhuǎn)換為一個(gè)實(shí)例對(duì)象的過(guò)程,其運(yùn)行后的輸出結(jié)果為:混世魔王。這太easy了,是的,這就是序列化和反序列化典型的demo。但此處隱藏著一個(gè)問(wèn)題:如果消息的生產(chǎn)者和消息的消費(fèi)者所參考的類(Person類)有差異,會(huì)出現(xiàn)何種神奇事件?比如:消息生產(chǎn)者中的Person類增加了一個(gè)年齡屬性,而消費(fèi)者沒有增加該屬性。為啥沒有增加?!因?yàn)檫@是個(gè)分布式部署的應(yīng)用,你甚至都不知道這個(gè)應(yīng)用部署在何處,特別是通過(guò)廣播(broadcast)方式發(fā)送消息的情況,漏掉一兩個(gè)訂閱者也是很正常的。
這個(gè)時(shí)候給Person.java類增加一個(gè)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 }直接運(yùn)行Consumer.java 會(huì)拋出?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)在這種序列化和反序列化的類不一致的情形下,反序列化時(shí)會(huì)報(bào)一個(gè)InvalidClassException異常,原因是序列化和反序列化所對(duì)應(yīng)的類版本發(fā)生了變化,JVM不能把數(shù)據(jù)流轉(zhuǎn)換為實(shí)例對(duì)象。接著刨根問(wèn)底:JVM是根據(jù)什么來(lái)判斷一個(gè)類版本的呢?
好問(wèn)題,通過(guò)SerialVersionUID,也叫做流標(biāo)識(shí)符(Stream Unique Identifier),即類的版本定義的,它可以顯式聲明也可以隱式聲明。顯式聲明格式如下:
1 private static final long serialVersionUID = XXXXXL;而隱式聲明則是我不聲明,你編譯器在編譯的時(shí)候幫我生成。生成的依據(jù)是通過(guò)包名、類名、繼承關(guān)系、非私有的方法和屬性,以及參數(shù)、返回值等諸多因子計(jì)算得出的,極度復(fù)雜,基本上計(jì)算出來(lái)的這個(gè)值是唯一的。
serialVersionUID如何生成已經(jīng)說(shuō)明了,我們?cè)賮?lái)看看serialVersionUID的作用。JVM在反序列化時(shí),會(huì)比較數(shù)據(jù)流中的serialVersionUID與類的serialVersionUID是否相同,如果相同,則認(rèn)為類沒有發(fā)生改變,可以把數(shù)據(jù)流load為實(shí)例對(duì)象;如果不相同,對(duì)不起,我JVM不干了,拋個(gè)異常InvalidClassException給你瞧瞧。這是一個(gè)非常好的校驗(yàn)機(jī)制,可以保證一個(gè)對(duì)象即使在網(wǎng)絡(luò)或磁盤中“滾過(guò)”一次,仍能做到“出淤泥而不染”,完美地實(shí)現(xiàn)類的一致性。
但是,有時(shí)候我們需要一點(diǎn)特例場(chǎng)景,例如:我的類改變不大,JVM是否可以把我以前的對(duì)象反序列化過(guò)來(lái)?就是依靠顯式聲明serialVersionUID,向JVM撒謊說(shuō)“我的類版本沒有變更”,如此,我們編寫的類就實(shí)現(xiàn)了向上兼容。我們修改一下上面的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之后再進(jìn)行序列化的Producer.java和反序列化的Consumer.java之后
是不會(huì)拋出InvalidClassException異常的.
剛開始生產(chǎn)者和消費(fèi)者持有的Person類版本一致,都是V1.0,某天生產(chǎn)者的Person類版本變更了,增加了一個(gè)“年齡”屬性,升級(jí)為V2.0,而由于種種原因(比如程序員疏忽、升級(jí)時(shí)間窗口不同等)消費(fèi)端的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 }此時(shí)雖然生產(chǎn)者和消費(fèi)者對(duì)應(yīng)的類版本不同,但是顯式聲明的serialVersionUID相同,反序列化也是可以運(yùn)行的,所帶來(lái)的業(yè)務(wù)問(wèn)題就是消費(fèi)端不能讀取到新增的業(yè)務(wù)屬性(age屬性)而已。
通過(guò)此例,我們的反序列化實(shí)現(xiàn)了版本向上兼容的功能,使用V1.0版本的應(yīng)用訪問(wèn)了一個(gè)V2.0版本的對(duì)象,這無(wú)疑提高了代碼的健壯性。我們?cè)诰帉懶蛄谢惔a時(shí),隨手加上serialVersionUID字段,也不會(huì)給我們帶來(lái)太多的工作量,但它卻可以在關(guān)鍵時(shí)候發(fā)揮異乎尋常的作用。
注意 顯式聲明serialVersionUID可以避免對(duì)象不一致,但盡量不要以這種方式向JVM“撒謊”。
?
總結(jié)
以上是生活随笔為你收集整理的[改善Java代码]养成良好习惯,显式声明UID的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 应该始终以PreparedStateme
- 下一篇: CentOS 7下的MariaDB Ma