概述
Java SE5的重大變化之一就是提出了泛型的概念。泛型實現了參數化類型的概念,使代碼可以應用於多種類型中。有很多原因促成了泛型的出現,而最主要的原因,就是為了構建容器類。我們很多時候在寫代碼的過程中,經常是遇到了同一個方法如果傳入或者傳出的參數類型過於單一,同樣的功能的方法我們要寫兩次甚至是多次,原因就是由於參數類型不能適應其他類型的應用。那么為了解決這個問題,Java設計者也是煞費苦心。
- 一個泛型的小例子
- 泛型中使用注意的方法特性
- 擦除與其解決方法
- 泛型技術實例
一、一個泛型的小例子
在Java SE5之前,為了寫一個通用的方法,也許得這樣子。用Object來作為一個標准,反正大家都是Object的子類。
1 package Generic; 2 3 public class Holder { 4 5 private Object a; 6 public Holder(Object a){ 7 this.a = a; 8 } 9 public void set(Object a){ 10 this.a = a; 11 } 12 public Object get(){ 13 return a; 14 } 15 public static void main(String args[]){ 16 Holder h1 = new Holder(new String()); 17 h1.set("ss"); 18 Holder h2 = new Holder(new Integer(1)); 19 h2.set(“asdf”); 20 } 21 }
大家發現這個問題沒有,既然Hodler h2的set方法的傳入參數是一個Object,那么這個參數就可以是String,Integer,Double,h2其實是想保存的一個Integer類型的數據,但由於Object的泛用性,導致了一個String類型的數據保存了進去。這么來說,這就是一個潛在的隱患了。
在有些情況下,我們確實希望容器能夠同時持有多種類型的對象。但是,通常來說,我們只會使用容器來存儲一種類型的對象。泛型的主要目的之一就是用來指定容器要持有什么類型的對象,而且有編譯器來保證類型的正確性。因此,與其使用Object,我們更喜歡暫時不指定類型,而是稍后再決定具體使用什么類型。要達到這個目的,需要使用類型參數,用尖括號括住,放在類名后面。然后在使用這個類的時候,再用實際的類型代替此類型參數。
1 package Generic; 2 3 public class Holder2<T> { 4 5 private T a; 6 public Holder2(T a){ 7 this.a = a; 8 } 9 public void set(T a){ 10 this.a = a; 11 } 12 public T get(){ 13 return a; 14 }15 }
二、泛型中使用注意的方法特性
由於Object的包容性,導致了參數可能的混亂。所以為了解決這個問題,泛型的提出是一個很好的方案。下面就來介紹下泛型的一些用法和特點,這些特性大都由於泛型的擦除機制所導致,擦除機制是指在運行過程中任何和類型有關的信息都會被擦除,所有在運行中集合的具體信息都被擦除成它們的原生類型。下面通過一個例子來了解:
1 public class TestF { 2 3 public static void main(String[] args) { 4 Class a1 = new ArrayList<Integer>().getClass(); 5 Class a2 = new ArrayList<String>().getClass(); 6 System.out.println(a1 == a2); 7 //結果是true 8 } 9 10 }
ArrayList<Integer>和ArrayList<String>的class其實在擦除后都是ArrayList。
1.不能有繼承的關系泛型的
Vector<Object> v = new Vector<String>(); //Type mismatch: cannot convert from Vector<String> to Vector<Object>
不能有繼承的關系泛型。聲明了一個Vector<Object>與Vector<String>這對組合和Object與String這對組合是完全不同的。Object和String這對的確有關系,Object是String的父類,從java的多態Object obj = new String()這個語句是沒有關系的。但是Vector<Object>和Vector<String>的組合就不是繼承關系,在編譯時,其實、Vector<Object> v =new Vector<String>實際上是Vector c = new Vector()這種效果,其實就是java泛型的擦除效果。
其實從另外一個角度去看,Vector<Object>其實就是告訴編譯器,Vector里面要放的是Object,但是在實例化的時候卻放入了String,雖然是繼承關系,但是還是不同的兩個class。當涉及到了集合,看問題的角度提升到 集合 的層面去觀察,里面的泛型類型就一定要一致了(在沒有使用通配符?的情況下要一致,使用了?后面詳細講)。
2.不能有聲明數組類型的泛型
為什么不能有聲明數組類型的泛型呢?我看了一下Java SE的官方文檔 http://docs.oracle.com/javase/tutorial/extra/generics/fineprint.html
里面有兩段代碼很清楚的闡釋了這個問題。
1 // Not really allowed. 2 List<String>[] lsa = new List<String>[10]; 3 Object o = lsa; 4 Object[] oa = (Object[]) o; 5 List<Integer> li = new ArrayList<Integer>(); 6 li.add(new Integer(3)); 7 // Unsound, but passes run time store check 8 oa[1] = li; 9 10 // Run-time error: ClassCastException. 11 String s = lsa[1].get(0);
假設泛型數組允許被建立,如果此時有一個List<String>的數組,但是經過一系類轉換后,居然把List<Integer>給賦給了 List<String>數組,這是明顯的錯誤,但是由於java泛型的擦除機制,程序代碼沒有報錯,此時已經有問題了。
解決這個問題,就是不要聲明數組類型的泛型,用通配符聲明。
1 // OK, array of unbounded wildcard type. 2 List<?>[] lsa = new List<?>[10]; 3 Object o = lsa; 4 Object[] oa = (Object[]) o; 5 List<Integer> li = new ArrayList<Integer>(); 6 li.add(new Integer(3)); 7 // Correct. 8 oa[1] = li; 9 // Run time error, but cast is explicit. 10 String s = (String) lsa[1].get(0);
由於通配符,程序一開始不知道List<?>具體是什么類型的的數組,這個時候我們將List<Integer>放入進去,編譯器還好能接受。最后一句,故意從List<Integer>去拿String,這是報了java.lang.ClassCastException的錯誤。
3.泛型類不能有static修飾方法
這個更加不用說了,泛型編譯后其實是擦除了很多信息,很多時候都沒有確定好參數類型,到了運行調用的時候才進行的添加的。而static是靜態關鍵字,被修飾的變量和方法都是在運行一開始就被加載到了虛擬機中,無需要再new對象,一直通過對象引用就好了。這兩種狀態是不一致的,所以不能在泛型類上面加上static。
4.泛型參數會由於參數的合集類型、個數、順序一樣而導致方法不能重載。
1 public static void applyVecotr(Vector<Integer> v,Integer i){ 2 //泛型里的參數是不能作為區分參數去overload(重載) 3 } 4 public static void applyVecotr(Vector<String> v,Integer i){ 5 //泛型里的參數是不能作為區分參數去overload(重載) 6 }
此時報錯 Erasure of method applyVecotr(Vector<Integer>, Integer) is the same as another method in type GenericTest
5.通配符?和泛型類型參數代表符號<T>、<K>、<V>等的關系和區別
在一個代碼中通配符?用於泛型代表任何的類型,而<T>、<K>、<V>也是一樣功能。但是<T>、<K>、<V>等符號有一個好處,就是可以運用到在整個方法甚至整個類中去,用來區分多個泛型。表示<T>所代表的符號都是同一個類型的參數,而<K>是另外的一種參數。但是對於泛型參數則需要在方法或者類的開頭聲明,而通配符?則不需要的。
1 public class DiffGenericParameter<K,V> { 2 3 private K k;//代表K為key值 4 private V v;//代表V為value值 5 public DiffGenericParameter(K k,V v){ 6 this.v = v; 7 this.k = k; 8 }; 9 }
三、擦除與其解決方法
在《java編程思想》中對擦除的核心動機做出了解釋:它使得泛化的客戶端可以用非泛化的類庫來使用,反之亦然,這經常被稱為“遷移兼容性”。在理想情況下,當所有事物都可以同時被泛化時,我們就可以專注於此。在現實中,即使程序員只編寫泛化代碼,他們也必須處理在Java SE5之前編寫的非泛型類庫。那些類庫的作者可能從沒有想過要泛化它們的代碼,或者可能剛剛開始接觸泛型。
因此Java泛型不僅必須支持向后兼容性,即現有的代碼和類文件仍舊合法,並且繼續保持其之前的含義;而且還要支持遷移兼容性,使得類庫按照它們自己的步調變為泛型的,並且當某個類庫變為泛型時,不會破壞依賴於它的代碼和應用程序。通過允許非泛型代碼與泛型代碼共存,擦除使得這種向着泛型的遷移成為可能。
盡管如此,但是我們還是可以通過反射得到一些蛛絲馬跡的。通過反射的getGenericParameterTypes去拿到泛型中的參數類型。
1 package Generic; 2 3 import java.lang.*; 4 import java.util.*; 5 6 public class GenericTest { 7 8 public static void main(String[] args) throws Exception { 9 10 /** 11 * 通過反射去獲取方法的泛型參數化類型,通過其他方式是無法知道泛型的參數類型,因為在泛型中 12 * 編譯后去掉了泛型化。 13 */ 14 Method method = GenericTest.class.getMethod("applyVector", Vector.class,Map.class); 15 Object[] ParameterTypes = method.getParameterTypes(); 16 //System.out.println(ParameterTypes); 17 /*for(Object ParameterType : ParameterTypes ){ 18 System.out.println(ParameterType); 19 }*/ 20 ParameterizedType type = (ParameterizedType) method.getGenericParameterTypes()[0]; 21 System.out.println("type.getRawType() = "+type.getRawType()); 22 System.out.println("type.getActualTypeArguments = "+type.getActualTypeArguments()[0]); 23 } 24 25 public static void applyVector(Vector<Date> v, Map<String,String> map){ 26 27 } 28 29 }
得到的結果是:
type.getRawType() = class java.util.Vector type.getActualTypeArguments = class java.util.Date
由於有了這個反射能得到泛型的參數類型后,還是挺興奮的。但是我們不能經常去使用反射,這畢竟還是一個耗時耗力的事情。所以在泛型的很多代碼編寫上就有新的嘗試了。
- 泛型的定義加上了上邊界和下邊界
- 加入Class類型的代碼
1.泛型的定義加上了上邊界和下邊界
因為擦除移除了類型信息,如果使用無界泛型參數調用的方法只是那些可以用Object調用的方法。如果能夠將某個參數限制為某個類型子集,我們就能調用這個類型子集去調用方法了。所以在Java泛型中用了extends這個關鍵字,但是此時extends和在普通情況下面所具有的意義是完全不同的。
1 package Generic; 2 3 import java.awt.Color; 4 5 interface HasColor{java.awt.Color getColor();} 6 7 class Colored<T extends HasColor>{ 8 T item; 9 Colored(T item){ this.item = item;} 10 T getItem(){ 11 return item; 12 } 13 java.awt.Color color(){return item.getColor();} 14 } 15 16 class Dimension{public int x , y, z;} 17 18 //this won't work -- class must be first, then interface; 19 //calss coloredDimension<T extends HasColor & Dimension> 20 21 class ColoredDimension<T extends Dimension & HasColor>{ 22 T item; 23 ColoredDimension(T item){this.item = item;} 24 T getItem(){return item;} 25 java.awt.Color getColor(){return item.getColor();} 26 int getX(){return item.x;} 27 int getY(){return item.y;} 28 int getZ(){return item.z;} 29 } 30 31 interface Weight{int weight();} 32 33 class Solid<T extends Dimension & HasColor & Weight>{ 34 T item; 35 Solid(T item){this.item = item;} 36 T getItem(){return item;} 37 java.awt.Color color(){return item.getColor();} 38 int getX(){return item.x;} 39 int getY(){return item.y;} 40 int getZ(){return item.z;} 41 int weight(){return item.weight();} 42 } 43 44 class Bounded extends Dimension implements HasColor ,Weight { 45 46 @Override 47 public int weight() { 48 // TODO Auto-generated method stub 49 return 0; 50 } 51 52 @Override 53 public Color getColor() { 54 // TODO Auto-generated method stub 55 return null; 56 } 57 58 } 59 60 public class BasicBounds { 61 62 public static void main(String[] args) { 63 Solid<Bounded> solid = new Solid<Bounded>(new Bounded()); 64 System.out.println(solid.weight()); 65 System.out.println(solid.color()); 66 System.out.println(solid.getY()); 67 } 68 69 }
所以你會發現,原來extends在泛型使用中不僅僅可以被用來限定上邊界class,還可以用來限定上邊界interface。而且,在此之后我們可以使用class或者interface中的類型變量或者是方法,這樣子我們就可以很好的利用Java提供的這個機制去構建一些泛型關系。
2.加入Class類型的代碼
既然被擦除了,那就在調用的時候再將這個類型傳入就好了。這個也是挺不錯的方法之一。
1 package Generic; 2 3 class ClassAsFactory<T>{ 4 T x; 5 public ClassAsFactory(Class<T> kind){ 6 try { 7 x= kind.newInstance(); 8 } catch (InstantiationException e) { 9 // TODO Auto-generated catch block 10 e.printStackTrace(); 11 } catch (IllegalAccessException e) { 12 // TODO Auto-generated catch block 13 e.printStackTrace(); 14 } 15 } 16 } 17 18 class Employee{} 19 20 public class InstantiateGenericType { 21 22 public static void main(String[] args) { 23 ClassAsFactory<Employee> fe = new ClassAsFactory<Employee>(Employee.class); 24 System.out.print("ClassAsFactory<Employee> succeed"); 25 ClassAsFactory<Integer> fi = new ClassAsFactory<Integer>(Integer.class); 26 27 } 28 }
發現了沒有,在new實例化這個泛型的時候,我們再將這個實例化的類型傳入,接着再將這個newInstance就好了。
下一篇,將介紹泛型用於程序設計中的實例,不錯的實例。
