改善Java程序的建議(每日5條)3

目錄

  • 建議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很正常

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章