Java 泛型

一、泛型由來
Java語言類型包括八種基本類型(byte short int long float double boolean char)和複雜類型,複雜類型包括類和數組。
早期Java版本(1.4之前)如果要代指某個泛化類對象,只能使用Object,這樣寫出來的代碼需要增加強轉,而且缺少類型檢查,代碼缺少健壯性。在1.5之後,Java引入了泛型(Generic)的概念,提供了一套抽象的類型表示方法。利用泛型,我們可以:
1、表示多個可變類型之間的相互關係:HashMap<T,S>表示類型T與S的映射,HashMap<T, S extends T>表示T的子類與T的映射關係

2、細化類的能力:ArrayList<T> 可以容納任何指定類型T的數據,當T代指人,則是人的有序列表,當T代指杯子,則是杯子的有序列表,所有對象個體可以共用相同的操作行爲

3、複雜類型被細分成更多類型:List<People>和List<Cup>是兩種不同的類型,這意味着List<People> listP = new ArrayList<Cup>()是不可編譯的。後面會提到,這種檢查基於編譯而非運行,所以說是不可編譯並非不可運行,因爲運行時ArrayList不保留Cup信息。另外要注意,即使People繼承自Object,List<Object> listO = new ArrayList<People>()也是不可編譯的,應理解爲兩種不同類型。因爲listO可以容納任意類型,而實例化的People列表只能接收People實例,這會破壞數據類型完整性。

4、簡化代碼實現:假設有一個執行過程,對不同類型的數據,進行某些流程一致的處理,不引入泛型的實現方法爲:
[java] view plain copy
 在CODE上查看代碼片派生到我的代碼片
  1. public void addToArray(Integer data, Integer array[], int pos) {  
  2.     array[pos] = data;  
  3. }  
  4. public void addToArray(Long data, Long array[], int pos) {  
  5.     array[pos] = data;  
  6. }  
這是一種典型的多態行爲——重載,但是不夠簡化。引入泛型的寫法更優雅:
[java] view plain copy
 在CODE上查看代碼片派生到我的代碼片
  1. public <T> void addToArray(T data, T array[], int pos) {  
  2.     array[pos] = data;  
  3. }  

二、泛型定義與使用(泛型類和泛型方法)
1、泛型參數的命名風格:
1)儘量用簡便的命名來命名泛型,若類型無特定意義,儘量使用一個字符
2)儘量使用全大寫來命名泛型形參,以此與其他類型區分開
3)單字母的泛型建議用T命名,如果有多個泛型,可以取T周圍的大寫字母,注意,如果泛型本身有意義,可以不遵守這一條,比如緩存管理CacheManager<PARAM, DATA>,該類負責管理緩存查詢條件與數據的映射,用單字母就不太合適,使用多字母更好
4)對於泛型函數或者泛型內部類在某個泛型類中出現的情況,建議泛型函數和內部類的泛型形參名稱與外層類的泛型名稱保持不同,否則容易引起混淆。類似這種:
[java] view plain copy
 在CODE上查看代碼片派生到我的代碼片
  1. public class GenericClass<T> {  
  2.      public <T> void testGenericMethod(T t) {  
  3.      }  
  4. }  

其實testGenericMethod方法的形參與外面GenericClass的形參完全沒有關係。換句話說,泛型方法的泛型是優先使用方法泛型定義的。這種更應該寫成:
[java] view plain copy
 在CODE上查看代碼片派生到我的代碼片
  1. public class GenericClass<T> {  
  2.      public <S> void testGenericMethod(S s) {  
  3.      }  
  4. }  

2、泛型存在兩種用法:泛型類和泛型方法

1)泛型類
定義泛型類時,在類名後加<>,尖括號內可以定義一個或多個泛型參數,並指定泛型參數的取值範圍,多個參數用逗號(,)分割
泛型類中定義的泛型全類可用(靜態方法、靜態代碼塊、靜態成員變量除外)
父類定義的泛型參數子類無法繼承,所以子類得自己寫
[java] view plain copy
 在CODE上查看代碼片派生到我的代碼片
  1. public class GenericClass<T extends AClass> {  
  2.     T data;  
  3.     void setData(T t) {  
  4.         data = t;  
  5.     }  
  6.     T getData() {  
  7.         return data;  
  8.     }  
  9. }  

2)泛型方法
定義泛型方法,在方法修飾符後,返回參數前加上<>,尖括號內可以定義一個或多個泛型參數,並指定泛型參數取值範圍,多個參數用逗號(,)分割
泛型方法中定義的泛型作用域在方法內
[java] view plain copy
 在CODE上查看代碼片派生到我的代碼片
  1. public class GenericMethodClass {  
  2.     public <T extends AClass, S extends T> T setData(T t, S s) {  
  3.         //do something    
  4.         return t;  
  5.     }  
  6. }  

定義泛型方法,更多是爲了表達返回值和方法形參間的關係,本例中方法第一個參數T繼承AClass,第二個參數S繼承T,返回值是第一個參數。
如果僅僅是爲了實現了多態,應優先使用通配符。類似如下:
[java] view plain copy
 在CODE上查看代碼片派生到我的代碼片
  1. public void addList(List<?> list) {  
  2.     //todo  
  3. }  

3、定義了多個泛型類型參數時,一定要在使用時都指定類型,否則會編譯出錯。

4、對泛型類的類型參數賦值包含兩種方法:
1)類變量或實例化:
List<String> listS;
listS = new ArrayList<String>();

2)繼承
public class MyList<S> extends ArrayList<S> implements IMyInterface<S> {
}
S是對ArrayList內部定義的泛型E的賦值。

5、對泛型方法的賦值:
[java] view plain copy
 在CODE上查看代碼片派生到我的代碼片
  1. public <T> T testMethod1(T t, List<T> list) {  
  2. }  
  3.   
  4. public <T> T testMethod2(List<T> list1, List<T> list2){  
  5. }  
  6.   
  7. People n = null;  
  8.   
  9. List<People> list1 = null;  
  10. testMethod1(n, list1);//此時泛型參數T爲People  
  11.   
  12. List<Integer> list2 = null;  
  13. testMethod2(list1, list2);//編譯報錯  

三、通配符
1)上述泛型賦值都是賦予泛型參數確定值,我們還可以賦予泛型參數不確定值,也就是通配符?。使用通配符?表示一個未知的類型。類似如下:
List<?> list;存放任意的對象
List<? extends AClass> listSubAClass; //存放AClass的子類
List<? extends BClass> listSuperBClass; //存放BClass的父類

2)通配符通常與泛型關鍵字一起使用。

3)在Java集合框架中,對於未知類型的容器類,只能讀取其中元素,不能添加元素。這是因爲,對不確定的參數類型,編譯器無法識別添加元素的類型和容器的類型是否兼容,唯一的例外是NULL。同時,其讀取的元素只能用Object來存儲。

4)通配符不能用在泛型類和泛型方法聲明中,類似如下:
[java] view plain copy
 在CODE上查看代碼片派生到我的代碼片
  1. public class GenericClass<?> { //編譯錯誤  
  2.   public <?> void testGenericMethod(? t) { //編譯錯誤  
  3.   }  
  4. }  

四、泛型關鍵字
1、泛型關鍵字有二個 extends和super,分別表示類型上界和類型下界
T extends AClass 表示T繼承自AClass類
? super AClass 表示?是AClass的父類,注意:super只能與通配符?搭配使用,我們不能寫:
public class GenericClass<T super AClass> { //錯誤
}
此例子中super換成extends是正確的,表示泛型T繼承自AClass,T換成通配符?也是可以的,表示未知類型的下界是AClass。

2、通配符與泛型關鍵字組合使用

舉兩個例子:
下界:
[java] view plain copy
 在CODE上查看代碼片派生到我的代碼片
  1. List<? super People> list = new ArrayList<>();  
  2. People people = new People();  
  3. list.add(people);  
  4. People data= list.get(0 ); //編譯出錯,報錯Object不能轉爲People  

上界:
[java] view plain copy
 在CODE上查看代碼片派生到我的代碼片
  1. List<? extends People> list = new ArrayList<>();  
  2. People people = new People();  
  3. list.add(people);// 編譯出錯,不能向容器中添加確定的元素  
  4. People data= list.get( 0);  

總結就是:上界添加(add)受限,下界查詢(get)受限

五、泛型實現原理
1、Java泛型是編譯時技術,在運行時不包含類型信息,僅其實例中包含類型參數的定義信息。
2、Java利用編譯器擦除(erasure,前端處理)實現泛型,基本上就是泛型版本源碼到非泛型版本源碼的轉化。
3、擦除去掉了所有的泛型類內所有的泛型類型信息,所有在尖括號之間的類型信息都被扔掉.
舉例來說:List<String>類型被轉換爲List,所有對類型變量String的引用被替換成類型變量的上限(通常是Object)。
而且,無論何時結果代碼類型不正確,會插入一個到合適類型的轉換。
[java] view plain copy
 在CODE上查看代碼片派生到我的代碼片
  1. <T> T badCast(T t, Object o) {  
  2. return (T) o; // unchecked warning  
  3. }  
這說明String類型參數在List運行時並不存在。它們也就不會添加任何的時間或者空間上的負擔。但同時,這也意味着你不能依靠他們進行類型轉換。

4、一個泛型類被其所有調用共享
對於上文中的GenericClass,在編譯後其內部是不存入泛型信息的,也就是說:
[java] view plain copy
 在CODE上查看代碼片派生到我的代碼片
  1. GenericClass<AClass> gclassA = new GenericClass<AClass>();  
  2. GenericClass<BClass> gclassB = new GenericClass<BClass>();  
  3. gClassA.getClass() == gClassB.getClass()  
這個判斷返回的值是true,而非false,因爲一個泛型類所有實例運行時具有相同的運行時類,其實際類型參數被擦除了。

那麼是不是GenericClass裏完全不存AClass的信息呢?這個也不是,它內部存儲的是泛型向上父類的引用,比如:
GenericClass<AClass extends Charsequence>, 其編譯後內部存儲的泛型替代是Charsequence,而不是Object。

那麼我們編碼時的泛型的類型判斷是怎麼實現的呢?
其實這個過程是編譯時檢查的,也就是說限制gClassA.add(new BClass()) 這樣的使用的方式的主體,不是運行時代碼,而是編譯時監測。

泛型的意義就在於,對所有其支持的類型參數,有相同的行爲,從而可以被當作不同類型使用;類的靜態變量和方法在所有實例間共享使用,所以不能使用泛型。

5、泛型與instanceof
泛型擦除了類型信息,所以使用instanceof檢查某個實例是否是特定類型的泛型類是不可行的:
GenericClass genericClass = new GenericClass<String>();
if (genericClass instanceof GenericClass<String>) {} // 編譯錯誤

同時:
GenericClass<String> class1 = (GenericClass<String>) genericClass; //會報警告

六、Class與泛型(摘自網絡)
從Java1.5後Class類就改爲了泛型實現,Class類內定義的泛型T指的是Class對象代表的類型。比如說String.class類型代表Class<String>,People.class類型代表Class<People>。
主要用於提高反射代碼的類型安全。

Class類的newInstance返回泛型T的對象,故而可以在反射時創建更精確的類型。
舉例來說:假定你要寫一個工具方法來進行一個數據庫查詢,給定一個SQL語句,並返回一個數據庫中符合查詢條件
的對象集合(collection)。
一個方法是顯式的傳遞一個工廠對象,像下面的代碼:
[java] view plain copy
 在CODE上查看代碼片派生到我的代碼片
  1. interface Factory<T> {  
  2.     public T[] make();  
  3. }  
  4. public <T> Collection<T> select(Factory<T> factory, String statement) {  
  5.      Collection<T> result = new ArrayList<T>();  
  6.      /* run sql query using jdbc */  
  7.      for ( int i=0; i<10; i++ ) { /* iterate over jdbc results */  
  8.          T item = factory.make();  
  9.          /* use reflection and set all of item’s fields from sql results */  
  10.          result.add( item );  
  11.       }  
  12.       return result;  
  13. }  

你可以這樣調用:
[java] view plain copy
 在CODE上查看代碼片派生到我的代碼片
  1. select(new Factory<EmpInfo>() {  
  2.       public EmpInfo make() {  
  3.           return new EmpInfo();  
  4.       }  
  5.   } , ”selection string”);  
也可以聲明一個類 EmpInfoFactory 來支持接口 Factory:
[java] view plain copy
 在CODE上查看代碼片派生到我的代碼片
  1. class EmpInfoFactory implements Factory<EmpInfo> {  
  2.       ...  
  3.       public EmpInfo make() {  
  4.             return new EmpInfo();  
  5.       }  
  6. }  
然後調用:
[java] view plain copy
 在CODE上查看代碼片派生到我的代碼片
  1. select(getMyEmpInfoFactory(), "selection string");  

這個解決方案的缺點是它需要下面的二者之一:
調用處那冗長的匿名工廠類,或爲每個要使用的類型聲明一個工廠類並傳遞其對象給調用的地方
這很不自然。
使用class類型參數值是非常自然的,它可以被反射使用。沒有泛型的代碼可能是:
[java] view plain copy
 在CODE上查看代碼片派生到我的代碼片
  1. Collection emps = sqlUtility.select(EmpInfo.class, ”select * from emps”);  
  2. ...  
  3. public static Collection select(Class c, String sqlStatement) {  
  4.           Collection result = new ArrayList();  
  5.           /* run sql query using jdbc */  
  6.           for ( /* iterate over jdbc results */ ) {  
  7.                 Object item = c.newInstance();  
  8.                 /* use reflection and set all of item’s fields from sql results */  
  9.                 result.add(item);  
  10.           }  
  11.           return result;  
  12.    }  
  13. }  

但是這不能給我們返回一個我們要的精確類型的集合。現在Class是泛型的,我們可以寫:
[java] view plain copy
 在CODE上查看代碼片派生到我的代碼片
  1. Collection<EmpInfo> emps=sqlUtility.select(EmpInfo.class, ”select * from emps”);  
  2. ...  
  3. public static <T> Collection<T> select(Class<T>c, String sqlStatement) {  
  4.      Collection<T> result = new ArrayList<T>();  
  5.      /* run sql query using jdbc */  
  6.      for ( /* iterate over jdbc results */ ) {  
  7.          T item = c.newInstance();  
  8.          /* use reflection and set all of item’s fields from sql results */  
  9.          result.add(item);  
  10.      }  
  11.      return result;  
  12. }  

籍此以類型安全的方式獲取我們需要的集合。
這項技術是一個非常有用的技巧,在處理註釋(annotations)的新API中被廣泛使用。

七、容器與泛型
Java泛型的最深入人心的應用就是容器(Collections)了。容器不需要考慮它要裝什麼東西,它的職責就是表達它裝的東西的集合所具有的功能。因此是天然的泛型支持者。
在沒有泛型時,如果要封裝一個列表,簡化應該是這樣的:
[java] view plain copy
 在CODE上查看代碼片派生到我的代碼片
  1. public class ArrayList {  
  2.     Object[] array = new Object[10];  
  3.     int i = 0;  
  4.     public void add(Object object) {  
  5.           array[i++] = object;  
  6.     }  
  7.   
  8.     public Object get(int index) {  
  9.           return array[index];  
  10.     }  
  11. }  

這意味着我們把元素存進去,取出來還要強轉,類型安全無法保證(存入一個Integer再存一個Long,轉出時強轉成Integer就崩潰了)。用泛型可以在編譯時保證不能存入非泛型支持的數據,保證類型安全。
按照我們之前說的,ArrayList內不存儲泛型信息,而是存儲泛型的最近父類,對ArrayList<T>而言就是Object,所以其內部代碼是:
[java] view plain copy
 在CODE上查看代碼片派生到我的代碼片
  1. public class ArrayList<T> {  
  2.     Object[] array = new Object[10];  
  3.     int i = 0;  
  4.     public void add(T object) {  
  5.           array[i++] = object;  
  6.     }  
  7.   
  8.     public T get(int index) {  
  9.           return (T)array[index];  
  10.     }  
  11. }  

保證我們加進去和取出來的數據都是經過類型檢查的。

八、總結
1、泛型是Java爲類型安全而做的一個優化,它在內部保證了數據類型一致性,細化了可變參數的類型,且能更好的表示類型間的相關性。
2、泛型是編譯時技術,其起作用的時機是編譯時,完善的編譯器會報告錯誤的泛型使用,保證代碼不能編譯通過。
3、平常寫代碼時,要認真思考是否有使用泛型的必要。通常來講,如果方法或類描述的是數據類型無關的邏輯,且其數據類型可變時,則應該使用泛型。

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