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方法確認屬性名字,而不是根據類裡宣告的屬性名決定;