1. 程式人生 > 實用技巧 >Java18(泛型,反射,內省)

Java18(泛型,反射,內省)

一、泛型

  泛型是JavaSE1.5的新特性,泛型的本質是引數化型別,也就是說操作的資料型別被指定為一個引數。這種引數型別可以用在類、介面和方法的建立中,分別稱為泛型類,泛型介面,泛型方法。

  Java語言引入泛型的最大好處就是安全簡單,可以將執行時型別相關的錯誤提前到編譯時錯誤。

  在沒有泛型之前,通過對型別Object的引用來實現引數的任意化,這種方式帶來的缺點就是需要使用顯示的強制型別轉換,而這種轉換是要求開發者對實際引數可以預知的情況下進行的。對於強制型別轉換來說,編譯時是不會報錯的,而是在執行時才會出現異常,所以存在安全隱患。

  泛型的好處就是在編譯時就會檢查型別安全,並且所有的強制型別轉換都是自動和隱式的,提高了程式碼的重用率。

  

  

  通常情況下,編譯器會有兩種處理泛型的方式:  

    1.Codespecialization

      在例項化一個泛型類或泛型方法時都產生一份新的目的碼(位元組碼or二進位制程式碼)。例如,針對一個泛型list,可能需要針對string,integer,float產生三份目的碼

    2.Codesharing

      對每個泛型類只生成唯一的一份目的碼;該泛型類的所有例項都對映到這份目的碼上,在需要的時候執行型別檢查和型別轉換

  Java編譯器通過Codesharing的方式為每個泛型建立唯一的位元組碼錶示,並且將該泛型型別的例項都對映到這唯一的位元組碼錶示上。將多種泛型型別例項都對映到唯一的位元組碼錶示是通過型別擦除

實現的。

  型別擦除:指的是通過型別引數合併,將泛型型別例項關聯到同一份位元組碼檔案上。編譯器只為泛型型別生成一份位元組碼檔案,並將例項關聯到這份位元組碼檔案上。型別擦除的關鍵在於從泛型型別中清除型別引數相關的資訊,並且在必要時新增型別檢查和型別轉換的方法。

  型別擦除可以簡單理解為,將泛型java程式碼轉換為普通java程式碼,只不過編譯器更直接點,將泛型java程式碼直接轉換成普通java位元組碼檔案。型別擦除的主要步驟:

    將所有的泛型引數用其最左邊界(最頂級的父類型別)型別替換

    移出所有的型別引數

  

  

  第一個泛型類Comparable<A>擦除後A被替換為最左邊界Object。Comparable<NumericValue>的型別引數NumericValue被擦除掉,但是這直接導致NumericValue沒有實現介面Comparable的compareTo(Objectthat)方法,於是編譯器充當好人,添加了一個橋接方法

  第二個示例中限定了型別引數的邊界<AextendsComparable<A>>A,A必須為Comparable<A>的子類,按照型別擦除的過程,先講所有的型別引數ti換為最左邊界Comparable<A>,然後去掉引數型別A,得到最終的擦除後結果

  

  

  

  所有的泛型類的泛型引數在編譯時都會被擦除,虛擬機器執行時沒有泛型,只有普通類和普通方法。

  Java泛型不支援基本資料型別,即ArrayList<int>是不被允許的

  在泛型程式碼內部,無法獲得任何有關泛型引數型別的資訊,如果傳入的型別引數,為T,那麼在泛型程式碼內部你不知道T有什麼方法,屬性,關於T的一切資訊都丟失了。

  建立泛型的時候需要指明型別,讓編譯器儘早的做引數檢查。

  忽略編譯器的警告,那意味著有潛在的ClassCastException

  Java的泛型型別不能用於new構建物件,(也不能用於初始化陣列)

  定義泛型方法的規則:

    1.型別引數宣告部分在方法返回值型別之前。

    2.每一個型別引數宣告部分包含一個或多個引數型別,引數見用逗號隔開。一個泛型引數,也被稱為一個型別變數,是用於指定一個泛型型別名稱的識別符號。

    3.泛型方法體的宣告和其他方法一樣,注意引數型別只能代表引用資料型別,不能是基本資料型別。

    4.型別引數能作為方法的返回值型別,並且能作為泛型方法得到的實際引數型別的佔位符

二、泛型類

   泛型類的宣告和非泛型類的宣告類似,除了在類名後面添加了型別引數宣告部分

  和泛型方法一樣,泛型類的型別引數宣告部分也包含一個或多個型別引數,引數間用逗號隔開

三、泛型介面

  

  

  

  陣列的協變:

    上例中main方法中的第一行,建立了一個 Apple 陣列並把它賦給 Fruit 陣列的引用。這是有意義的,Apple 是 Fruit 的子類,一個 Apple 物件也是一種 Fruit 物件,所以一個 Apple 陣列也是一種 Fruit 的陣列。

  儘管Apple[]可以“向上造型”為Fruit[],但陣列中元素的實際型別還是Apple,我們只能向陣列中新增Apple或者Apple的子類。雖然Fruit陣列中可以加入Orange陣列,編譯器也不會報錯,但是,在執行時就會出現異常,因為JVM知道陣列的實際型別是Apple[]。

  

  上面程式碼無法編譯,儘管Apple是Fruit的子型別,但是ArrayList<Apple>不是ArrayList<Fruit>的子型別,泛型不支援協變

  

  上述例子中,List的型別是List<? extends Fruit>,我們可以把他當做:一個型別的List,這個型別可以使繼承了Fruit的某種型別。他表示:“某種特定的型別,但List沒有指定”。例如:list引用可以指向某個型別的List,只要這個型別繼承自Fruit,可以使Fruit或者Apple,比如例子中的new ArrayList<Apple>,但是為了向上轉型給list,list並不關心這個具體型別是什麼。

  Fruit是它的上邊界,而且我們並不知道這個List到底持有什麼型別,所以除了Null之外的其他型別都是不安全的。所以如果做了泛型的向上轉型,(List<? extends Fruit> flist = new ArrayList<Apple>()),我們也就失去了對這個list新增任何物件的能力。

  如果呼叫某個方法返回Fruit的方法,這是安全的,因為在list中,不管它實際的型別到底是什麼,都肯定能轉型為Fruit,所以編譯器允許返回Fruit。

  

  上面的例子中,flist 的型別是List<? extends Fruit>,泛型引數使用了受限制的萬用字元,所以我們失去了向其中加入任何型別物件的例子,最後一行程式碼無法編譯

  但是 flist 卻可以呼叫 contains 和 indexOf 方法,它們都接受了一個 Apple 物件做引數。如果檢視 ArrayList 的原始碼,可以發現 add() 接受一個泛型型別作為引數,但是 contains 和 indexOf 接受一個 Object 型別的引數,所以如果我們指定泛型引數為 <? extends Fruit> 時,add() 方法的引數變為 ? extends Fruit,編譯器無法判斷這個引數接受的到底是 Fruit 的哪種型別,所以它不會接受任何型別,然而,contains 和 indexOf 的型別是 Object,並沒有涉及到萬用字元,所以編譯器允許呼叫這兩個方法。這意味著一切取決於泛型類的編寫者來決定那些呼叫是 “安全” 的,並且用 Object 作為這些安全方法的引數。如果某些方法不允許型別引數是萬用字元時的呼叫,這些方法的引數應該用型別引數,比如 add(E e)

  

  

  

  還有一種萬用字元是無邊界萬用字元,它的使用形式是一個單獨的問號:List<?>,也就是沒有任何限定

  • List<?> list 表示 list 是持有某種特定型別的 List,但是不知道具體是哪種型別。那麼我們可以向其中新增物件嗎?當然不可以,因為並不知道實際是哪種型別,所以不能新增任何型別,這是不安全的。而單獨的 List list ,也就是沒有傳入泛型引數,表示這個 list 持有的元素的型別是 Object,因此可以新增任何型別的物件,只不過編譯器會有警告資訊 

四、反射

  • Java反射機制允許程式在執行時通過Reflection APIs取得任何一個已知名稱的class的內部資訊,包括其修飾符、父類、介面、構造方法、屬性、方法等,並可於執行時改變屬性值或者呼叫方法等;
  • Java反射機制是Java語言的一個重要特性,使得Java語言具備“動態性”: JAVA反射機制是構建框架技術的基礎所在,例如後續學習的Spring框架等,都使用到反射技術;
    • 在執行時獲取任意一個物件所屬的類的相關資訊;
    • 在執行時構造任意一個類的物件;
    • 在執行時獲取任意一個類所具有的成員變數和方法;
    • 在執行時呼叫任意一個物件的方法;
  • Java的反射機制依靠反射API實現,反射API主要包括以下幾個類,後續學習: java.lang.Class類是反射機制中最重要的類,是使用反射機制時的“起點”;
    • java.lang.Class類:代表一個類;
    • java.lang.reflect.Field 類:類的成員變數(成員變數也稱為類的屬性);
    • java.lang.reflect.Method類:類的方法;
    • java.lang.reflect.Constructor 類:類的構造方法;
    • java.lang.reflect.Array類:動態建立陣列,以及訪問陣列的元素的靜態方法;
  • JVM執行程式時,會將要使用到的類載入到記憶體中,同時就會自行為這個類建立一個Class物件,這個物件中就封裝了類的所有資訊,包括類中的屬性、方法、構造方法、修飾符等;
  • Java.lang.Class類中定義了一系列的getXXX方法,可以獲取Class中封裝的其他資訊;

  

  

  

  

  獲取Class類物件:

 1 package com.chinasofti.reflect;
 2 
 3 public class newClass {
 4     public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
 5 
 6         // 利用反射建立類(class)物件的三種方式:
 7         // 1.
 8         Class<Student> studentClass = Student.class;
 9 
10         // 2.
11         Class<?> aClass = Class.forName("com.chinasofti.reflect.Student");
12 
13         // 3.
14         Student s = new Student();
15         Class<? extends Student> aClass1 = s.getClass();
16 
17         // 對比一下三種方式建立的物件是否一致
18         System.out.println(studentClass==aClass && aClass==aClass1);
19 
20         // 使用class物件的newInstance方法建立物件
21         Student student = studentClass.newInstance();
22         Student student1 = (Student) aClass.newInstance();
23         Student student2 = aClass1.newInstance();
24     }
25 }

  獲取構造方法物件:

  

 1 package com.chinasofti.reflect;
 2 
 3 import java.lang.reflect.Constructor;
 4 import java.lang.reflect.InvocationTargetException;
 5 
 6 public class newConstructor {
 7     public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
 8         // 先獲取class物件
 9         Class<Student> studentClass = Student.class;
10         // 獲取class物件的無參構造器物件
11         Constructor<Student> constructor = studentClass.getConstructor();
12         // 使用無參構造器的物件來建立物件
13         Student student = constructor.newInstance();
14         System.out.println(student);
15 
16         System.out.println("--------------------");
17         // 通過指定引數獲得指定有參構造器的物件
18         Constructor<Student> constructor1 = studentClass.getConstructor(String.class, int.class);
19         // 建立有參物件 這裡的引數是一個Object型別的可變引數
20 //        Student s = constructor1.newInstance(new Object[]{"張三", 231});
21         Student student1 = constructor1.newInstance("張三", 123);
22         System.out.println(student1);
23     }
24 }

  獲取方法: 

 1 package com.chinasofti.reflect;
 2 
 3 import java.lang.reflect.InvocationTargetException;
 4 import java.lang.reflect.Method;
 5 
 6 public class getMethods {
 7     public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
 8         Class<Student> studentClass = Student.class;
 9         // 通過class物件獲取指定名稱的方法物件
10         Method getName = studentClass.getMethod("getName");
11         // 使用方法物件中的invoke方法來呼叫方法    invoke(Object obj, Object... args)  第一個引數是物件 第二個引數是可變引數,用於傳參
12         Object invoke = getName.invoke(studentClass);
13         System.out.println(invoke);
14     }
15 }

  一個簡易的api方法:

 1 package com.chinasofti.reflect;
 2 
 3 import java.lang.reflect.Constructor;
 4 import java.lang.reflect.InvocationTargetException;
 5 import java.lang.reflect.Method;
 6 
 7 public class UtilMethod {
 8     public static void main(String[] args) {
 9         Object getName = start("com.chinasofti.reflect.Student", "getName");
10         System.out.println(getName);
11     }
12 
13 
14     /**
15      * 反射呼叫方法 返回方法呼叫後的返回值
16      * @param className 類名
17      * @param methodName 方法名
18      * @param arr 方法引數列表
19      * @return 方法的返回值
20      */
21     public static Object start(String className, String methodName, Object ... arr) {
22         // 宣告一個Object型別的返回值
23         Object res = null;
24         try {
25             // 建立一個class物件
26             Class<?> aClass = Class.forName(className);
27             // 獲取class物件的無參構造方法
28             Constructor<?> constructor = aClass.getConstructor();
29             // 使用無參構造方法來建立物件
30             Object o = constructor.newInstance();
31             // 通過class物件獲取全部方法
32             Method[] methods = aClass.getMethods();
33             // 開始遍歷 尋找符合引數中的方法名稱的方法
34             for (Method method:methods) {
35                 if(method.getName().equals(methodName)){
36                     // 找到該方法 然後使用invoke呼叫 獲取引數
37                     res = method.invoke(o,arr);
38                     break;
39                 }
40             }
41         } catch (ClassNotFoundException e) {
42             e.printStackTrace();
43             return null;
44         } catch (InstantiationException e) {
45             e.printStackTrace();
46             return null;
47         } catch (InvocationTargetException e) {
48             e.printStackTrace();
49             return null;
50         } catch (NoSuchMethodException e) {
51             e.printStackTrace();
52             return null;
53         } catch (IllegalAccessException e) {
54             e.printStackTrace();
55             return null;
56         }
57         // 返回呼叫方法後的返回值
58         return res;
59     }
60 }

五、內省

  在實際程式設計中,我們常常需要一些用來包裝值物件的類,例如Student、Employee、Order。這樣類中往往沒有業務方法,只是為了把需要處理的實體物件進行封裝,有這樣的特徵:

    1.屬性都是私有的;

    2.有無參的public構造方法;

    3.對私有屬性根據需要提供公有的getXxx方法以及setXxx方法;例如屬性名稱為name,則有getName方法返回屬性name值,setName方法設定name值;注意方法的名稱通常是get或set加上屬性名稱,並把屬性名稱的首字母大寫;這些方法稱為getters/setters;getters必須有返回值沒有方法引數;setter值沒有返回值,有方法引數;

  符合這些類特徵的類,被稱為JavaBean;

  內省機制就是基於反射的基礎,Java語言對Bean類屬性、事件的一種預設處理方法;

    與Java內省有關的主要類及介面有:

      1.java.beans.Introspector類:為獲得JavaBean屬性、事件、方法提供了標準方法;通常使用其中的getBeanInfo方法返回BeanInfo物件。

      2.java.beans.BeanInfo介面:不能直接例項化,通常通過Introspector類返回該型別物件,提供了返回屬性特徵描述符物件(PropertyDescriptor)、方法描述符物件(MethodDescriptor)、bean描述符(BeanDescriptor)物件的方法;

      3.java.beans.PropertyDescriptor類:用來描述一個屬性,該屬性有getter及setter方法。

  

  • 只要類中有getXXX方法,或者setXXX方法,或者同時有getXXX及setXXX方法,其中getXXX方法沒有方法引數,有返回值;setXXX方法沒有返回值,有一個方法引數;那麼內省機制就認為XXX為一個屬性;

  

  

 1 package com.chinasofti.reflect;
 2 
 3 import org.apache.catalina.util.Introspection;
 4 
 5 import java.beans.*;
 6 
 7 // 內省 當一個getXXX方法沒有引數 有返回值 或者一個setXXX方法有引數 沒有返回值 那麼內省機制就認為該XXX是屬性
 8 public class IntrospectionTest {
 9     public static void main(String[] args) throws IntrospectionException {
10         // 通過內省物件獲取beaninfo物件(使用Student的class物件反射獲取)
11         BeanInfo beanInfo = Introspector.getBeanInfo(Student.class);
12         // 通過beaninfo物件 獲取所有屬性儲存到陣列中
13         PropertyDescriptor[] propertyDescriptors = beanInfo.getPropertyDescriptors();
14         // 遍歷輸出內省機制所認為的屬性名
15         for (PropertyDescriptor p:propertyDescriptors) {
16             System.out.println(p.getName());
17         }
18 
19         // 獲得類class物件
20         BeanDescriptor beanDescriptor = beanInfo.getBeanDescriptor();
21         System.out.println("----" + beanDescriptor.getName());
22         // 獲取該類的方法集合
23         MethodDescriptor[] methodDescriptors = beanInfo.getMethodDescriptors();
24         for (MethodDescriptor m:methodDescriptors) {
25             System.out.println(m.getName());
26         }
27     }
28 }

  • 很多框架都使用了內省機制檢索物件的屬性,定義屬性名字時,名字最好起碼以兩個小寫字母開頭,例如stuName,而不要使用sName,某些情況下,可能會導致檢索屬性失敗;
  • 再次強調,內省機制檢索屬性時,是根據getter和setter方法確認屬性名字,而不是根據類裡宣告的屬性名決定;