在Java語言處於尚未出現泛型的版本時,只能經過Object是全部類型的父類和類型強制轉換兩個特色的配合來實現類型泛化。例如在哈希表的存取中,JDK1.5以前使用HashMap的get()方法,返回值就是一個Object對象,因爲Java語言裏面全部的類型都繼承於java.lang.Object,那Object轉型爲任何對象成都是有可能的。可是也由於有無限的可能性,就只有程序員和運行期的虛擬機才知道這個Object究竟是個什麼類型的對象。在編譯期間,編譯器沒法檢查這個Object的強制轉型是否成功,若是僅僅依賴程序員去保障這項操做的正確性,許多ClassCastException的風險就會被轉嫁到程序運行期之中。java
泛型是JDK1.5的一項新特性,它的本質是將類型參數化,簡單的說就是將所操做的數據類型指定爲一個參數,在用到的時候經過傳參來指定具體的類型。在Java中,這種參數類型能夠用在類、接口和方法的建立中,分別稱爲泛型類、泛型接口和泛型方法。程序員
一個泛型類的例子以下:segmentfault
//將要操做的數據類型指定爲參數T public class Box<T> { private T t; public void add(T t) { this.t = t; } public T get() { return this.t; } }
//使用的時候指定具體的類型爲Integer //那麼Box類裏面的全部T都至關於Integer了 Box<Integer> integerBox = new Box<Integer>();
泛型接口和泛型方法的定義和使用示例以下:數組
//泛型接口 interface Show<T,U> { void show(T t,U u); } class ShowTest implements Show<String,Date> { @Override public void show(String str,Date date) { System.out.println(str); System.out.println(date); } } public static void main(String[] args) { ShowTest showTest = new ShowTest(); showTest.show("Hello",new Date()); }
//泛型方法 public <T, U> T get(T t, U u) { if (u != null) return t; else return null; } String str = get("Hello", "World");
從上面的例子能夠看出,用尖括號<>來聲明泛型變量,能夠有多個類型變量,例如Show<T, U>,可是類型變量名不能重複,例如Show<T, T>是錯誤的。另外,類型變量名通常使用大寫形式,且比較短(不強制,只是一種命名規約),下面是一些經常使用的類型變量:框架
類型限定就是使用extends關鍵字對類型變量加以約束。好比限定泛型參數只接受Number類或者子類Integer、Float等,能夠這樣限定
//定義一個水果類 //裏面有一個示例方法getWeight()能夠獲取水果重量 public class Fruit { public int getWeight() { return 10; //這裏假設全部水果重量都是10 } } public class Apple extends Fruit {} -------------------------------------------------------------------------------------------- //定義泛型類Box,並限定類型參數爲Fruit public class Box<T extends Fruit> {} -------------------------------------------------------------------------------------------- //因爲Box限定了類型參數,實際類型參數只能是Fruit或者Fruit的子類 Box<Fruit> integerBox = new Box<Fruit>();//編譯經過 Box<Apple> integerBox = new Box<Apple>();//編譯經過 Box<Integer> integerBox = new Box<Integer>();//編譯器報錯
上面代碼用虛線分爲三個部分,第一個部分是舉例用的,定義一個水果類Fruit和它的子類Apple;第二部分定義一個泛型類Box,而且限定了泛型參數爲Fruit,限定以後,實際類型只能是Fruit或者Fruit的子類,因此第三部分,實際泛型參數是Integer就會報錯。學習
經過限定,箱子Box就只能裝水果了,這是有好處的,舉個例子,好比Box裏面有一個getBigFruit()方法能夠比較兩個水果大小,而後返回大的水果,代碼以下:ui
public class Box<T extends Fruit>{ public T getBigFruit(T t1, T t2) { // if (!(t1 instanceof Fruit) || !(t2 instanceof Fruit)) { // throw new RuntimeException("T不是水果"); // } if (t1.getWeight() > t2.getWeight()) { return t1; } return t2; } }
代碼中須要注意兩個地方:一個是註釋的三行,參數限定以後,不必判斷t1和t2的類型了,若是類型不對,在Box實例化的時候就報錯了;另外一個是t1.getWeight(),在Box類裏面,t1是T類型,T類型限定爲Fruit,因此這裏能夠直接調用Fruit裏面的方法getWeight()(確切的說是能夠調用Fruit裏面能夠被子類繼承的方法,由於限定以後,實參也能夠是Fruit的子類),若是不加限定,那麼T就默認是Object類型,t1.getWeight()就會報錯由於Object裏面沒有這個方法(調用Object裏面的方法是能夠的)。這就是是類型限定的兩個好處。this
類型也可使用接口限定,好比
先看三行代碼
Fruit f = new Apple(); Fruit[] farray = new Apple[10]; ArrayList<Fruit> flist = new ArrayList<Apple>();
第一行的寫法是很常見的,父類引用指向子類對象,這是java多態的表現。相似的,第二行父類數組的引用指向子類數組對象在java中也是能夠的,這稱爲數組的協變。Java把數組設計爲協變的,對此是有爭議的,有人認爲這是一種缺陷。
雖然Apple[]能夠「向上轉型」爲Fruit[],但數組元素的實際類型仍是Apple,因此只能向數組中放入Apple或者Apple的子類。在上面的代碼中,向數組中放入了Fruit對象和Orange對象,對於編譯器來講,這是能夠經過編譯的,可是在運行時期,JVM可以知道數組的實際類型是Apple[],因此當其它對象加入數組的時候在運行期會拋出異常。
由上可知,協變的缺陷在於可能的異常發生在運行期,而編譯期間沒法檢查,泛型設計的目的之一就是避免這種問題,因此泛型是不支持協變的,也就是說,上面的第三行代碼是編譯不經過的。可是,有時候是須要創建這種「向上轉型」的關係的,好比定義一個方法,打印出任意類型的List中的全部數據,示例以下:
public void printCollection(List<Object> collection) { for (Object obj : collection) { System.out.println(obj); } } ------------------------------------ List<Integer> listInteger =new ArrayList<Integer>(); List<String> listString =new ArrayList<String>(); printCollection(listInteger); //編譯錯誤 printCollection(listString); //編譯錯誤
由於泛型不支持協變,即List<Object> collection = new ArrayList<Integer>();
沒法經過編譯,因此printCollection(listInteger)
就會報錯。
這時就須要使用通配符來解決,通配符<?>,用來表示某種特定的類型,可是不知道這個類型究竟是什麼。例以下面的例子都是合法的:
List<?> collection1 = new ArrayList<Fruit>(); List<?> collection2 = new ArrayList<Number>(); List<?> collection3 = new ArrayList<String>(); List<?> collection4 = new ArrayList<任意類型>(); // 對比不合法的 List<Fruit> flist = new ArrayList<Apple>();
因此printCollection()方法改爲下面這樣便可:
public void printCollection(List<?> collection) { for (Object obj : collection) { System.out.println(obj); } }
這就是通配符的簡單用法。須要注意的是,由於不知道 "?" 類型究竟是什麼,因此List<?> collection
中的collection不能調用帶泛型參數的方法,可是能夠調用與泛型參數類型無關的方法,以下:
collection.add("a"); //錯誤,由於add方法參數是泛型E collection.size(); //正確,由於無參即與泛型參數類型E無關 collection.contains("a"); //正確,由於contains參數是Object類型,與泛型參數類型E無關
注:collection.add(null);是能夠的,除了null其餘任何類型都不能夠,Object也不行。
通配符可使用extends和super關鍵字來限制:
注意區分 泛型變量的類型限定 和 通配符的邊界限定:
泛型變量的類型限定只能使用extends關鍵字,通配符的邊界限定可使用extends或super來限定上邊界或下邊界。
Java中的泛型是經過類型擦除來實現的僞泛型。類型擦除指的是從泛型類型中清除類型參數的相關信息,而且在必要的時候添加類型檢查和類型轉換的方法。類型擦除能夠簡單的理解爲將泛型java代碼轉換爲普通java代碼,只不過編譯器更直接點,將泛型java代碼直接轉換成普通的java字節碼,看下面的例子:
泛型的Java代碼以下:
class Pair<T> { private T value; public T getValue() { return value; } public void setValue(T value) { this.value = value; } }
泛型Java代碼,通過編譯器編譯後,會擦除泛型信息,將泛型代碼轉換爲以下的普通Java代碼:
class Pair { private Object value; public Object getValue() { return value; } public void setValue(Object value) { this.value = value; } }
由上面的例子可知,泛型擦除的結果就是用Object替換T,最終生成一個普通的類。上面的例子替換成Obejct是由於在Pair
至此能夠知道,類型擦除的過程:
泛型只存在於代碼中,泛型信息在編譯時都會被擦除,因此虛擬機中沒有泛型,只有普通類和普通方法。
使用泛型時會有一些問題和限制,大部分是由類型擦除引發的,因此只要記住:泛型擦除後留下的只有原始類型。那麼大部分問題都是很容易理解的。好比下面的例子:
public void test(List<String> list){ } public void test(List<Integer> list){ }
兩個方法通過泛型擦除後,都只留下原始類型List,因此它們是同一個方法而不是方法的重載,若是這兩個方法在同一個類中同時存在,編譯器是會報錯的。
其餘更多問題以下:
這些問題的答案在:java泛型(二)、泛型的內部原理:類型擦除以及類型擦除帶來的問題,本文也是學習這篇博客和一些其餘博客後的一個總結。
參考文章:
Java泛型-類型擦除
java泛型(一)、泛型的基本介紹和使用
java泛型(二)、泛型的內部原理:類型擦除以及類型擦除帶來的問題
Java 泛型總結(三):通配符的使用