一、泛型
泛型是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方法確認屬性名字,而不是根據類里聲明的屬性名決定;