1.Java的泛型是類型擦除的
Java中的泛型是在編譯期間有效的,在運行期間將會被刪除,也就是所有泛型參數類型在編譯後都會被清除掉.請看以下例子
- public static void test(List<Integer> testParameter) {
- }
- public static void test(List<String> testParameter) {
- }
以上是一個最最簡單的重載例子,在編譯後泛型類型是會被擦除的,所以無論是List<Integer>或List<String>都會變成List<E>,所以參數相同,重載不成立,無法編譯通過,而且讀者可以嘗試將不同泛型的類getClass()看看,他們的Class是一樣的
2.不能初始化泛型參數和數組
請觀察以下代碼
- class Test<T> {
- //編譯不通過
- private T t = new T();
- //編譯不通過
- private T[] tArray = new T[5];
- //編譯通過
- private List<T> list = new ArrayList<T>();
- }
Java語言是一門強類型,編譯型的安全語言,它需要確保運行期的穩定性和安全性,所以在編譯時編譯器需要嚴格的檢查我們所聲明的類型,在編譯期間編譯器需要獲取T類型,但泛型在編譯期間已經被擦除了,所以new T()和new T[5]都會出現編譯錯誤,而爲什麼ArrayList卻能成功初始化?這是因爲ArrayList表面是泛型,但在編譯期間已經由ArrayList內部轉型成爲了Object,有興趣的讀者可以看一下ArrayList的源碼,源碼中容納元素的是private transient Object[] elementData,是Object類型的數組
3.強制聲明泛型的實際類型
在使用泛型時,在大多數情況下應該聲明泛型的類型,例如該集合是存放User對象的,就應該使用List<User>來聲明,這樣可以儘量減少類型的轉換,若只使用List而不聲明泛型,則該集合等同於List<Object>
4.注意泛型沒有繼承說
請看以下例子
- public static void main(String[] args) {
- // 編譯不通過
- List<Object> list = new ArrayList<Integer>();
- // or
- List<Object> list = new ArrayList<Double>();
- }
上面代碼的業務邏輯爲有一組元素,但我不確定元素時整形還是浮點型,當我在確定後創建對應的泛型實現類,剛接觸泛型的讀者有可能會犯以上錯誤,理解爲泛型之間是可以繼承的,例如聲明Object的泛型實際上創建Integer泛型,錯誤認爲泛型之間也存在繼承關係,但這是不正確的,泛型是幫助開發者編譯期間類型檢查安全.我們可以換種方式實現以上業務邏輯
- public static void main(String[] args) {
- //Number 爲Integer 和 Double 的公共父類
- List<Number> numberList = new ArrayList<Number>();
- //使用通配符,代表實際類型是Number類型的子類
- List<? extends Number> numberList2 = new ArrayList<Integer>();
- //or
- List<? extends Number> numberList3 = new ArrayList<Double>();
- }
5.不同場景使用不同的通配符
泛型當中支持通配符,可以單獨使用 '?' 來表示任意類型,也可以使用剛纔上面例子中的extends關鍵字表示傳入參數是某一個類或接口的子類,也可以使用super關鍵字表示接受某一個類型的父類型,extends和super的寫法一樣,extends是限定上界,super爲限定下界
6.建議採用順序爲List<T> List<?> List<Object>
以上三者都可以容納所有的對象,但使用的順序應該是首選List<T>,然後List<?>,最後才使用List<Object>,原因是List<T>是確定爲某一類型的,安全的,也是Java所推薦的,而List<?>代表爲任意類型,與List<T>類似,而List<Object>中全部爲Object類型,這與不使用泛型無異,而List<T>是可讀可寫的,因爲已經明確了T類型,而其他兩者在讀取時需要進行向下轉型,造成了不必要的轉型
7.使用多重邊界限定
現在有一個業務需求,收錢時需要是我們本公司的員工(繼承User),同時亦需要具備收銀員的功能(實現Cashier),此時我們當然可以想到接受1個User然後判斷User是否實現了Cashier接口,但在泛型的世界中,我們可以怎麼做?請看以下例子
以上例子就表明,限定了上界,只要是User和Cashier的子類纔可以作爲參數傳遞到方法當中
======================我是泛型和反射的分界線====================
8.注意Class類的特殊性
Java語言是先把Java源文件編譯成後綴爲class的字節碼文件,然後通過ClassLoader機制把類文件加載到內存當中,最後生成實例執行的,在描述一個類是,Java使用了一個元類來對類進行描述,這就是Class類,他是一個描述類的類,所以註定Class類是特殊的
1.Class類無構造函數,Class對象加載時由JVM通過調用類加載器的
defineClass方法來構造Class對象
2.Class類還可以描述基本數據類型,由於基本類型並不是Java中的對象,它們
一般存在於棧,但Class仍然可以對它們進行描述,例如使用int.class
3.其對象都是單例模式,一個Class的實現對象描述一個類,並且只描述一個類
所以只要是該被描述的類所有對象都只有一個Class實例
4.Class類是Java反射的入口
9.適時選擇getDeclaredXXX和getXXX
Class類中提供了很多getDeclaredXXX和getXXX的方法,請看以下例子
- public static void main(String[] args) {
- Class<User> cls = User.class;
- //獲取類方法
- cls.getDeclaredMethods();
- cls.getMethods();
- //獲取類構造函數
- cls.getDeclaredConstructors();
- cls.getConstructors();
- //獲取類屬性
- cls.getDeclaredFields();
- cls.getFields();
- }
getXXX的方式是獲取所有公共的(public)級別的,包括從父類繼承的方法,而getDeclaredXXX的方式是獲取所有的,包括公共的(public),私有的(private),不受限與訪問權限,
- Type[] t1 = m.getParameterTypes();// 其返回是參數的類型
- Type[] t2 = m.getGenericParameterTypes();//其返回的是參數的參數化的類型,裏面的帶有實際的參數類型
10.反射訪問屬性或方法時將Accessible設置爲true
在調用構造函數或方法的invoke前檢查accessible已經是公認的寫法,例如以下代碼
- public static void main(String[] args) throws Exception {
- Class<User> cls = User.class;
- //創建對象
- User user = cls.newInstance();
- //獲取test方法
- Method method = cls.getDeclaredMethod("test");
- //檢查Accessible屬性
- if(!method.isAccessible()){
- method.setAccessible(true);
- }
- method.invoke(user);
- }
讀者可以嘗試獲取Class的getMethod,也就是公開的方法,再輸出isAccessible,可以看到輸出的其實也是false,其實因爲accessible屬性的語義並不是我們理解的訪問權限,而是指是否進行安全檢查,而安全監察是非常消耗資源的,所以反射提供了Accessible可選項,讓開發者逃避安全檢查,有興趣的讀者可以查看AccessibleObject類觀察其源碼瞭解安全檢查.
11.使用forName動態加載類
forName相信各位讀者不會陌生,在使用JDBC時要動態加載數據庫驅動就是使用forName的方式進行加載,同時亦可以從外部配置文件中讀取類的全路徑字符串進行加載,在使用forName時,被加載的類就會被加載到內存當中,只會加載類,並不會執行任何代碼,而我們的數據庫驅動就是利用static代碼塊來執行操作的,因爲當類被加載到內存中時,會執行static代碼塊
12.使用反射讓模板方法更強大
模板方法的定義是,定義一個操作的算法骨架,將某些步驟延遲到子類當中實現,而實現細節由子類決定,父類只決定骨架,以下是一個傳統模板方法的事例
- public abstract class Test {
- public final void doSomething() {
- System.out.println("start...");
- doInit();
- System.out.println("end.....");
- }
- protected abstract void doInit();
- }
此時子類只需要繼承Test類實現doInit()方法即可嵌入到doSomething中,現在我們有一個需求,若我在doSomething中需要調用一系列的方法才能完成doSomething呢?而且我調用方法的數量並不確定,只需遵從某些規則則可將方法添加到doSomething方法當中.請看以下代碼
- public abstract class Test {
- public final void doSomething() throws Exception {
- Method[] methods = this.getClass().getDeclaredMethods();
- System.out.println("start...");
- for (Method method : methods) {
- if (this.checkInitMethod(method)) {
- method.invoke(this);
- }
- }
- System.out.println("end.....");
- }
- private boolean checkInitMethod(Method method) {
- // 方法名初始是否爲init
- return method.getName().startsWith("init")
- // 是否爲public修飾
- && Modifier.isPublic(method.getModifiers())
- // 返回值是否爲void
- && method.getReturnType().equals(Void.TYPE)
- // 是否沒有參數
- && !method.isVarArgs()
- // 是否抽象類型
- && !Modifier.isAbstract(method.getModifiers());
- }
- }
看到上面的代碼,讀者是否有似曾相識的感覺?在使用Junit3時是不是隻要遵守方法的簽名約定,就能成爲測試類?使用這種反射可以讓模板方法更強大,下次需要使用多個方法在模板方法中時,不要創建多個抽象方法,嘗試使用以上方式
13.不要過分關注反射的效率
反射的效率是一個老生常談的問題,普通的調用方法,創建類,在反射的情況下需要調用諸多API才能實現,效率當然要比普通情況下低下,但在項目當中真正引起性能問題的地方,絕大數不會是由反射引起的,而反射帶給我們的卻是如此的美妙,當今的各系列開源框架,幾乎都存在反射的身影,而且大量存在更比比皆是,讓Java這個沉睡的美女活起來的,非反射莫屬