目錄
- 建議11:顯示聲明UID
- 建議12:避免用序列化類的構造函數中爲常量賦值
- 建議13:避免爲final變量複雜賦值
- 建議14:break的必記
- 建議15:避免Instanceof非預期結果
建議11:顯示聲明UID
我們編寫了一個實現Serializable接口(序列化標誌接口)的類,Eclipse就會給你一個黃色警告:需要增加一個SerialVersion ID。爲什麼要增加就在以下說明。
類實現Serializable 接口的目的是爲了可持久化,比如網絡傳輸或本地存儲,爲系統的分佈和異構部署提供先決支持條件。若沒有序列化,我們的遠程調用、對象數據庫就都不能存在。
public class Test implements Serializable{
private String name;
public String getName(){
return this.name;
}
public void setName(String name){
this.name = name;
}
}
這是一個簡單的JavaBean,實現了Serializable接口,可以在網絡上傳輸,也可以本地存儲然後讀取。這裏我們展示以Java消息服務方式傳遞該對象。首先定義一個消息的生產者
public class Producer{
public static void main(String[] args) throws Exception{
Test test = new Test();
test.setName("魔鬼");
// 序列化,保存到磁盤上
Utils.wirteObject(test);
}
}
class Utils{
private static String FILE_NAME = "d:/a.bin";
public static void writeObject(Serializable s){
try{
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(FILE_NAME));
oos.writeObject(s);
}catch(Exception e){
e.printStackTrace();
}finally{
oos.close();
}
}
public static Obejct readObject(){
Object obj = null;
//反序列化
try{
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(FILE_NAME));
obj = ois.readObject();
}catch(Exception e){
e.printStackTrace();
}finally{
ois.close();
}
}
}
通過對象序列化過程,把一個對象從內存塊轉化爲可傳輸的數據流,然後通過網絡發送到消息消費者,並進行反序列化,生成實例對象
public class Customer{
public static void main(String[] args) throws Exception{
// 反序列化
Test test = (Test)Utils.readObject();
System.out.println(test.getName());
}
}
這是一個對象數據流轉換爲一個實例對象的過程 結果是 魔鬼
但是此處隱藏一個問題:如果消息的生產者和消息的消費者所參考的類Test有差異,會出現什麼事情?譬如說消息生產者中增加了一個年齡屬性,而消費者中沒有增加該屬性。爲什麼沒增加,因爲這可能是一個分佈式部署的應用。這樣的話反序列化會報一個InvalidClassException異常,原因是序列化所對應類版本發生了變化,JVM不能把數據流轉換爲實例對象。
這就要說JVM是根據什麼來判斷一個類的版本吶?
- SerialVersionUID也叫做流標識符即類的版本定義的,可以顯式聲明也可以隱式聲明
顯式聲明
private static final lonf serialVersionUID = XXXXXL;
隱式生命就是我不聲明,編譯器在編譯的時候幫我生成。生成的依據是通過包名、類名、繼承關係、非私有的方法和屬性,以及參數、返回值等諸多因子計算得出的,極度複雜,基本上計算出來的值是唯一的。
serialVersionUID的作用就是。JVM在反序列化時,會比較數據流中的與類中的是否一致,如果一致則表示沒有發生改變。可以實例對象,否則拋異常。
有時候我們的類改變不大,就要求JVM是否可以把以前的對象反序列化回來,就依靠顯式聲明serialVersionUID,向JVM通知我的版本沒有變,實現向上兼容。
也就是在Test類中加入
private static final long serialVersionUID = 2333L;
【注意】顯式聲明可以避免對象不一致,但是儘量不要用這種方式。
建議12:避免用序列化類的構造函數中爲常量賦值
帶有final標識的屬性是不變量,也就是說只能賦值一次,不能重複賦值,但是在序列化類中有點複雜。
public class Test implements Serializable{
private static final long serialVersionUID = 71282334L;
public final String name="魔鬼";
}
這個Test類被絮硫化存儲在磁盤,反序列化時name會被重新計算值(與static變量不同,static變量根本就沒保存到數據流中),比如name屬性修改成"天使",那麼反序列化對象的name值就是"天使".保持新舊對象final變量相同,有利於代碼業務邏輯統一,保證序列化基本規則之一。這種方式不多說
另一種賦值方式:通過構造方法
public class Test implements Serializable{
private static final long serialVersionUID = 71282334L;
public final String name;
public Test(){
this.name = "魔鬼";
}
}
這也是一種我們常用的賦值方式,我們試試序列化並做一個簡單的模擬,修改name值代表變更,這裏注意serialVersionUID不變 第一次輸入"魔鬼",第二次改爲"天使"
那麼打印結果是什麼,是"魔鬼"。因爲這裏觸及了反序列化的另一個規則:反序列化時構造函數不會執行。
反序列化過程
- JVM從數據流中獲取一個Object對象,然後根據數據流中的類文件描述信息(在序列化時,保存到磁盤的對象文件包含了類描述信息,不是類)查看發現是final變量,需要重新計算,於是引發Test類的name值,此時JVM發現name沒有賦值,不能引用,於是他就沒有初始化,而是保持原值。所以結果就是"魔鬼"
【注意】:在序列化類中,不要用構造函數爲final變量賦值
建議13:避免爲final變量複雜賦值使
爲final變量賦值的方式還有一種是通過方法賦值
public class Test implements Serializable{
private static final long serialVersionUID = 71282334L;
public final String name = initName();
public String initName(){
return "魔鬼";
}
}
name屬性是通過initName方法的返回值賦值的,這在複雜類中經常用到,這比使用構造函數賦值更簡潔,易修改,那麼如此用法在序列化時是否會有問題?
以下是修改後的Test類代碼
public class Test implements Serializable{
private static final long serialVersionUID = 71282334L;
public final String name = initName();
public String initName(){
return "天使";
}
}
僅僅修改了initName返回值,也就是說通過new生成的Test對象的final變量值都是"天使",把之前存儲在磁盤的士力架再下來,打印下name值會是什麼,結果會是"魔王"
上個建議說final變量會被重新賦值,這個值指的是簡單對象(8個基本類型,數組,字符串(特指不通過new創建的情況下),final變量的賦值與基本類型相同),但是不能方法賦值
其中的原理是
- 類描述信息
保存了包路徑、繼承關係、訪問權限、變量描述、變量訪問權限、方法簽名、返回值、以及變量的關聯類信息。這些是爲了保證反序列化的正常運行 - 非瞬態(transient關鍵字)和非瞬態(static關鍵字)的實例變量值
值如果是基本類型的就簡單保存下來,如果是複雜對象,連該對象和關聯類信息一起保存,並且持續遞歸下去。
【注意】反序列化時final變量在以下情況下不會被重新賦值: - 通過構造函數final變量賦值
- 通過方法返回值爲final變量賦值
- final修飾的屬性不是基本類型。
建議14:break的必記
我們經常寫一些轉換類由器是將1轉換爲"壹"等操作那就要寫工具類了
public class Test{
public static String toChineseNumberCase(int n){
String chinsesNumber = "";
switch(n){
case 1: chineseNumber ="一";
case 2: chineseNumber ="二";
case 3: chineseNumber ="三";
case 4: chineseNumber ="四";
case 5: chineseNumber ="五";
case 6: chineseNumber ="六";
case 7: chineseNumber ="七";
case 8: chineseNumber ="八";
case 9: chineseNumber ="九";
}
return chineseNumber;
}
}
我們運行看看輸入2=九??
回頭再看程序程序從case2 後面的語句開始執行一直找break關鍵字可惜我們沒有寫,一直到switch結束。
這種問題十分容易出現,對於這種問題個人建議就是將編輯器IDE的警告級別switch case調到Errors級別沒寫就報錯。
建議15:避免Instanceof非預期結果
instanceof是一個簡單的二元操作符,是用來判斷一個對象是否是一個類實例的,其操作類似於>=、==。
public class Test{
public static void main(String[] args){
boolean b1 = "String" instanceof Object;
boolean b2 = new String() instanceof String;
boolean b3 = new Object() instanceof String;
boolean b4 = 'A' instanceof Character;
boolean b5 = null instanceof String;
boolean b6 = new Date() instanceof String;
boolean b7 = new GenericClass<String>().isDateInstance("");
}
}
class GenericClass<T>{
public boolean isDateInstance(T t){
return t instanceOf Date;
}
}
boolean b1 = “String” instanceof Object;
返回值是true 因爲字符串繼承了Object 所以是true
boolean b2 = new String() instanceof String;
返回值是true 一個類的對象當然是它的實例
boolean b3 = new Object() instanceof String;
返回值是false Object是父類,其對象當然不是String的實例
boolean b4 = ‘A’ instanceof Character;
返回值false’A’是char類型,也就是一個基本類型,不是一個對象,instanceof只能用於對象判斷,不能用於基本類型判斷
boolean b5 = null instanceof String;
返回值是false,這是instanceof特有規則,左操作數是null就直接返回false
boolean b6 = new Date() instanceof String;
編譯通不過,因爲 Date類和 String沒有繼承或實現關係
boolean b7 = new GenericClass().isDateInstance("");
編譯通過了返回值是false,因爲Java泛型是爲編碼服務的,在編譯成字節碼是,T已經是Object類型了,傳遞的實數是String類型,也就是說T表面類型是Object,實際類型是String,這就話就等價於 Object instanceof Date 所以返回false很正常