先來看一下以下 2 段代碼,然後再進一步引出我們的泛型。
public static void main(String[] args) { List list = new ArrayList(); list.add("123"); list.add(456); Iterator it = list.iterator(); while(it.hasNext()){ // Error : Integer cannot be cast to String String next = (String)it.next(); } }
上面這段代碼,會出現轉化異常的情況,但是編譯是沒問題的,在輸出轉化的時候卻出現了異常,有沒有一種衝動想要把集合中的類型歸一?下面就是很正常的一個求和的方法,然而我們只能求類型為 Integer 的參數的和。
public Integer add(Integer a,Integer b){ return a + b;}
對於集合來說,我們若是能在編譯時間期指定該集合中存放資料的類型,這樣在類型轉化的時候就不會再出現錯誤了,同樣的,在下面的求和方法中,這個方法我們只能求得類型為 Integer 的參數的和,我們能不能做到可以通用的求和呢?使用泛型,就可以做到。
泛型的概念也就是 “ 資料類型參數化 ” 正是由於我們不確定集合中可以指定的類型有哪些,是 Integer 還是 String ?求和方法中參數的資料類型可以有哪些,是 Float 還是 Double ?那我們就可以使用泛型來把這個資料類型給參數化。
泛型的應用有泛型介面,泛型類和泛型方法。下面定義一個泛型類,並示範使用方式。
public class Box <T> { // T 是 Type 的簡寫,代表任意類型,注意是類,而不是基礎資料型別 (Elementary Data Type)。 // 也可以換成其它單詞,這隻是一個表示而已。 T t; public T getT() { return t; } public void setT(T t) { this.t = t; } // 在下面的應用中,我們可以將 T 換成任意我們想要的類型 public static void main(String[] args) { Box<Integer> iBox = new Box<Integer>(); Box<Double> dBox = new Box<Double>(); // 在 JDK1.7 及其以上版本可以利用 “類型推斷” 這樣寫。 Box<String> stringBox = new Box<>(); }}
泛型方法的定義只需要在方法的聲明中添加 < T > 即可,或是添加一組泛型 <K ,V> 。
public class Util { public static <K, V> boolean compare(Pair<K, V> p1, Pair<K, V> p2) { return p1.getKey().equals(p2.getKey()) && p1.getValue().equals(p2.getValue()); }}public class Pair<K, V> { private K key; private V value; public Pair(K key, V value) { this.key = key; this.value = value; } public void setKey(K key) { this.key = key; } public void setValue(V value) { this.value = value; } public K getKey() { return key; } public V getValue() { return value; }}
我們可以這樣來調用泛型方法。
Pair<Integer, String> p1 = new Pair<>(1, "apple");Pair<Integer, String> p2 = new Pair<>(2, "pear");boolean same = Util.<Integer, String>compare(p1, p2);
以上也就是簡單的應用,那我們還會遇到什麼情況呢,下面看一看關於萬用字元的問題,為了示範效果,我寫了一個實際用處不大的方法。
public static void printSize(List<Object> list){ System.out.println(list.size()); } public static void main(String[] args) { List<Integer> list1 = new ArrayList<>(); List<String> list2 = new ArrayList<>(); printSize(list1); // 報錯 }
上面我們已經知道,在集合中使用泛型可以使程式更加安全,易讀,所以 Java 規範推薦我們使用泛型,那我們看一下上面這種情況,我該如何表示接收所有帶有泛型的 List 集合呢。上面使用 LIst<Object> 是不行的。為什麼不行?我們該使用何種方式接收參數呢?
首先來解釋一下為什麼不行,先看一下簡單的區別。
Object obj = new Integer(1);ArrayList<Object> list = new ArrayList<Integer>(); // 報錯
obj 可以賦值成功,是因為多態(往深了說是裡氏替換原則),父類的引用指向了子類的實體,而下面的報錯了,直觀的理由說明,ArrayList<Object> 不是 ArrayList<Integer> 的父類。嗯,確實不是,為什麼不是,一句話帶過,是因為 Java 中正常的泛型是不變的,當然我們也可以使其改變。(不變,協變和逆變的概念可以自行百度)
傳送門:www.cnblogs.com/keyi/p/6068921.html
那我就是想讓方法接收所有的帶泛型的集合該怎麼辦呢?這時候萬用字元就出現了,我們可以使用 List<?> 代表所有的帶泛型的 List ,這樣也就是說可以使用 List<?> 來指向所有的帶泛型的 LIst 。
public static void printSize(List<?> list){ System.out.println(list.size()); } public static void main(String[] args) { List<Integer> list1 = new ArrayList<>(); List<String> list2 = new ArrayList<>(); printSize(list1); printSize(list2); }
好的,再次梳理一下邏輯,在 Java 中我們可以使用父類的引用指向子類的對象,而在泛型中,List<Object> 和 List<Integer> 不構成繼承關係,原因是因為泛型是不可變的,然而我們又希望表示所有帶有泛型的集合,這時就出現了 ?萬用字元。我們可以使用 List<?> 來引用其它帶泛型的 List 。
實際的效果就是這樣
List<Object> list1 = new ArrayList<Integer>(); // 報錯List<?> list2 = new ArrayList<Integer>();
那好,現在要求升級了,我希望我的 List 集合不要什麼都可以指向,下面就看一下一些有限制條件的修飾符該如何表示。
// 通用修飾符List<?> list1 = new ArrayList<Integer>();// <? extends T> 可用於表示 T 以及 T 的子類 List<? extends Number> list2 = new ArrayList<Number>();List<? extends Number> list3 = new ArrayList<Integer>();List<? extends Number> list4 = new ArrayList<String>(); // 報錯// <? super T> 可用於表示 T 以及 T 的父類 List<? super Number> list5 = new ArrayList<Number>();List<? super Number> list6 = new ArrayList<Object>();List<? super Number> list7 = new ArrayList<Integer>(); //報錯
對於上面的 <? extends Number> 和 <? super Number> 該如何選擇呢 ?先說結論:” Producer Extends,Consumer Super ” 簡稱 PECS 原則。
“Producer Extends” – 如果你需要一個唯讀 List,用它來 produce T,那麼使用< ? extends T > 。
“Consumer Super” – 如果你需要一個唯寫 List,用它來 consume T,那麼使用< ? super T > 。
如果需要同時讀取以及寫入,那麼我們就不能使用萬用字元了。
List<? extends Number> list = new ArrayList<Number>();List<? extends Number> list = new ArrayList<Integer>();List<? extends Number> list = new ArrayList<Double>();// 不論具體的執行個體化是什麼,我們 get 元素之後都是父類 Number// 所以是 produce ,可以 get 得到很多的 TNumber number = list.get(0);list.add(new Integer(1)); // 報錯
由於以上的三種執行個體化方式都是允許的,那麼假如我現在想從 list 中 get 一個執行個體,因為 list 指向的執行個體可能是 Animal ,Dog,或 Cat 執行個體的集合。所以返回的值會統一為其父類。而在 add 值的時候就會存在問題,我不能確定添加的元素具體是哪一個,除了 null ,所以會報錯。
同樣的思路再來看< ? super T > 操作。
List<? super Integer> list = new ArrayList<Integer>();List<? super Integer> list = new ArrayList<Number>();List<? super Integer> list = new ArrayList<Object>();// 不同的執行個體化,我們 get 元素之後返回的值不確定// 或是 Integer, Number, Object ……list.get(0); // 添加資料的時候可以確定的添加是什麼// 所以 super 對應唯寫入的情況,即 consume Tlist.add(new Integer(1));
關於泛型還要說明的是泛型是應用在編譯時間期的一項技術,而在運行期間是不存在泛型的。原因在於泛型型別擦除。為什麼這麼說,我們可以來看個樣本
public static void main(String[] args) { List<Integer> list = new ArrayList<Integer>(); list.add(456); list.add("123");// 編譯報錯 } -------------------------------------------------- public static void main(String[] args) { List<String> l1 = new ArrayList<String>(); List<Integer> l2 = new ArrayList<Integer>(); System.out.println(l1.getClass()); System.out.println(l1.getClass().equals(l2.getClass())); } // class java.util.ArrayList // true
究其原因,在於 Java 中的泛型這一概念提出的目的,導致其只是作用於代碼編譯階段,在編譯過程中,正確檢驗泛型結果後,會將泛型的相關資訊擦出,也就是說,成功編譯過後的 class 檔案中是不包含任何泛型資訊的。泛型資訊不會進入到運行時階段。
在類型擦除之後,若是在代碼中有相應的類型變數,遵循 " 保留上界 " 規則,會將相應的 T 替換成具體的類。
< ? > ---- > Object
< ? extends T > ---- > T
< ? super T > ----- > Object
補充說明一點,Java 中不允許直接建立泛型數組。
List<Integer>[] lists = new ArrayList<Integer>(); // 報錯看以下示範代碼// Not really allowed.List<String>[] lsa = new List<String>[10]; //1Object o = lsa;Object[] oa = (Object[]) o;List<Integer> li = new ArrayList<Integer>();li.add(new Integer(3));// Unsound, but passes run time store checkoa[1] = li;// Run-time error: ClassCastException.String s = lsa[1].get(0);
如果允許泛型數組的存在(第 1 處代碼編譯通過),那麼在第 2 處代碼就會報出 ClassCastException,因為 lsa[1] 是 List<Integer> 。Java 設計者本著首要保證型別安全(type-safety)的原則,不允許泛型數組的存在,使得編譯期就可以檢查到這類錯誤。
解決方案
List<?>[] lsa = new List<?>[10]; //1Object o = lsa;Object[] oa = (Object[]) o;List<Integer> li = new ArrayList<Integer>();li.add(new Integer(3));// Correct.oa[1] = li;// Run time error, but cast is explicit.String s = (String) lsa[1].get(0); //2
在第 1 處,用 ? 取代了確定的參數類型。根據萬用字元的定義以及 Java 類型擦除的保留上界原則,在 2 處 lsa[1].get(0) 取出的將會是 Object,所以需要程式員做一次顯式的類型轉換。
還有一種通過反射的方式來實現,使用 java.util.reflect.Array,可以不使用萬用字元,而達到泛型數組的效果。
List<String>[] lsa = (List<String>[])Array.newInstance(ArrayList.class, 4); //1Object o = lsa;Object[] oa = (Object[]) o;List<Integer> li = new ArrayList<Integer>();li.add(new Integer(3));// Correct.oa[1] = li;// Run time error, but cast is explicit.String s = lsa[1].get(0); //2
可以看到,利用 Array.newInstance() 產生了泛型數組,這裡沒有使用任何萬用字元,在第 2 處也沒有做顯式的類型轉換,但是在第 1 處,仍然存在顯式類型轉換。
所以要想使用泛型數組,要求程式員必須執行一次顯示的類型轉換,也就是將類型檢查的問題從編譯器交給了程式員。但是呢,泛型的設計初衷就是編譯器會協助我們檢查資料類型。你說矛盾不矛盾!
參考資料:
http://www.importnew.com/24029.html
51982979
www.cnblogs.com/wxw7blog/p/7517343.html
www.cnblogs.com/keyi/p/6068921.html
52058708