在面向對象的世界裏,咱們若是須要一個容器來盛裝對象。舉個例子:一個籃子。咱們能夠用這個籃子裝蘋果,也能夠用這個籃子裝香蕉。基於 OOP 的思想,咱們不但願爲蘋果和香蕉分別建立不一樣的籃子;同時,咱們但願放進籃子裏的是蘋果,拿出來的仍是蘋果。因而,Java 程序員提出了「泛型」的概念——一種相似於 C++ 模板的技術。java
早期程序員使用以下代碼建立一個泛型集合:程序員
public class ArrayList{ private Object[] elementData; ... public Object get(int i); public void add(Object o); }
咱們能夠看出,對與這個集合而言,取出 (get) 和放入時都沒有進行類型檢查。所以,若是咱們不記得放入的順序,把取出的對象進項強制類型轉換,極可能出現 ClassCastException
。所以,真正的泛型是能夠在編譯時期,對數據類型進行檢查,保證安全。以下面代碼所示:算法
ArrayList<String> list = new ArrayList<>();
P.S. <>
裏的String
叫作類型參數。segmentfault
使用泛型,爲咱們提供了以下優勢:數組
List<String> list = new ArrayList<>(3); String str = list.get(0);
泛型中使用名爲泛型參數代表能夠傳入類或方法的對象類型,這種設計實現了類型參數化(能夠把同一類型的類做爲參數進行傳遞),以下面的代碼所示:安全
泛型類示例框架
public class Pair<T>{ private T first; private T last; public Pair(){} public Pair(T first, T last){ this.first = frist; this.last = last; } public T getFirst(); public T getLast(); }
泛型方法示例this
public class Util{ // 簡單的泛型方法 public static <T> T getMiddle(T...a){ return a[a.length/2]; } // 帶限定符的泛型方法,若是有多個限定符,使用 & 鏈接多個接口或超類 public static <T extends Comparable> T min(T...a){ // 具體實現 } }
注意,這裏的泛型參數(type parameter)在上述示例中指的是用大寫字母 T 表示的值,而泛型實參(type argument)則是指 T a
中的 a
。根據慣例,泛型參數一般以下命名:spa
原始類型指的是,不包括泛型參數的類型,如上述泛型類中的 Pair
。咱們能夠經過原生類型構造對象:設計
Pair pair = new Pair();
同時,能夠經過泛型參數構造對象:
Pair<String> pair = new Pair<>();
可是,若是把一個經過原生類型獲取的對象指向一個經過泛型參數生成的參數會報 unchecked warning
,以下面的代碼:
Pair pair = new Pair(); Pair<String> pair1 = pair;
在 Java 中,有繼承的概念,簡而言之,就是一個類型能夠指向它的兼容類型,如:
Object object = new Object(); Integer integer = new Integer(20); object = integer;
上述代碼表示:Integer IS-A
Object。這種概念在泛型中也適用。以下定義:
public class Box<T extends Number>{ public void add(T t); }
那麼一個 Box 的對象能夠增長任意 Number 子類的值。可是 Box<Double>
和 Box<Integer>
不是同一個類型。
泛型類中能夠定義靜態、非靜態的泛型方法。泛型方法的語法爲:<泛型參數類型列表> + 返回類型 + 泛型參數列表。
public static <T> void foo(T t){ }
public void foo(T t){ }
在某種狀況下,咱們但願方法只接受特定類型的參數,可使用以下語法實現:
public <U extends Number> void inspect(U u){ // 這裏是邏輯處理 }
上述代碼中,該泛型方法只接受爲 Number 類型的參數。一樣,也能夠在泛型類上加以限制:
public class Utils<T extends Number>{ // 這裏的 T 必須爲 Number 類型 private T t; }
固然,也可使用多重限制,以下面代碼所示:
public class Utils<T extends A & B & C>{ }
P.S. 限制中的類必須放在接口的前面。
類型推斷是:編譯器去推斷調用方法的參數的類型的能力。
如,泛型方法中:
public <U> void addBox(Box<U> box){ // 這裏是處理代碼 }
沒必要經過 obj.<U>addBox(box)
調用,<U>
能夠省略。
構造方法中:
// 類型推斷 Map<String,List<String>> map = new HashMap<>();
其中,構造方法中的泛型還能夠這樣用:
// 定義泛型類 public class Box<X>{ public <X> Box(T t){ } } // 實例化一個對象 public class Application{ void method(){ Box<Integer> box = new Box<>("); } }
通配符 ?
表示一個未知的類型,可用於參數的類型、字段以及局部變量中,但不可用於調用泛型方法裏的類型參數、泛型對象實例化以及泛型超類裏。
// 能夠 public void foo(Pair<? extends Number> pair){ // 能夠 Pair<? super Integer> foo; } // 能夠 private Pair<? super Integer> pair;
上界通配符代表須要最高限定的類型,下面的代碼用來計算全部類型爲數字的集合的總和:
public double sumList(List<? extends Number>){ // 這裏作邏輯處理 }
使用無界限通配符表示不肯定的類型,如下兩種狀況可使用無界限通配符:
好比,有一個打印集合對象的方法:
// 定義一個打印集合對象列表的方法 public void printList(List<?> list){ for(Object obj: list){ // 打印list } } // 調用方法 List<Integer> integers = Arrays.asList(1,2,3); List<String> strings = Arrays.asList("A","B","C"); printList(integers); printList(strings);
P.S. List<?>
和 List<Object>
不一樣,List<?>
只能插入 null
而 List<Object>
能夠插入任何對象。
使用下界統配符,代表最低限度的類型,如:
public double sumList(List<? super Duble>){ // 這裏作邏輯處理 }
在本文的繼承和子類裏,提到過:Box<Double>
不是 Box<Number>
的子類。在 Java 泛型中,繼承關係能夠經過以下圖表示:
能夠看出,泛型中的 extends
的確限定了上界(父類);super
的確限定了下界(子類型);?
是全部泛型的超類(相似 Object)。
泛型的繼承關係(父子類型關係)能夠經過下面的韋恩圖解釋:
咱們不妨用某一泛型所佔的面積表示其層次關係,面積大的在繼承關係上層次高。由上圖很容易看出:<? super Integer>
的繼承層次比 <? super Number>
的繼承層次高;相應地,<? extends Integer>
的繼承層次比 <? extends Number>
的繼承層次低。
調用一個方法:foo(src, dest);
把 src
看作入參,dest
看作出參,基於如下規則決定是否使用和如何使用泛型:
extends
super
這種原則也叫作 PECS(Producer Extends Consumer Super) 原則。
類型擦除確保被參數化的類型不會建立新的類,不會產生運行時的開銷。
泛型擦除時,編譯器作了一點小小的工做:若是該泛型參數有邊界限制,替換成它的邊界;不然,用 Object 替換。
上述泛型類 Pair<T>
會被替換成下面形式:
class Pair{ Object first; Object last; public Object getFirst(){} public Object getLast(){} }
P.S. 通常使用第一個限定類型替換變爲原始類型,沒有限定類型,使用 Object 替換。
當子類繼承(或實現)父類(或接口)的泛型方法時,在子類中指明瞭具體的類型。編譯器會自動構建橋接方法(bridge method)。如:
class Node<T>{ private T t; public Node(T t){ setT(t); } public void setT(T t){ this.t = t; } } class MyNode extends Node<Integer>{ public MyNode(Integer i){ super(i); } public void setT(Integer i){ super.setT(i); } }
在上述代碼中,編譯時期,因爲泛型擦除,Node 中的方法爲 setT(Object t)
而 MyNode 中的方法爲 setT(Inetger i)
。簽名不匹配,再也不是重寫,所以,編譯器爲 MyNode 生成以下橋接方法:
// 橋接方法 public void setT(Object i){ setData((Integer)i); } public void setT(Integer i){ super.setData(i); }
具體類型(Reifiable Type)指的是:原始數據類型、非泛型類型、原生類型和調用不受限的通配等在運行時期,信息不會丟失的類型。
非具體類型(Non-Reifiable Type)在運行時期不能獲取其全部的信息,如 JVM 沒法區別 List<String>
和 List<Integer>
。所以,這種類型不能使用相似 instanceof
的方法。
堆污染指的是:一個參數化類型指向一個非該參數化類型對象的過程。一般是,在程序中進行了一些操做,使編譯時期發生未檢查(unchecked)警告時發生。如:混用原始類型(Raw Type)和參數化類型。
當使用可變參數做爲泛型輸入參數時,會形成堆污染。如:
能夠經過以下註解消除編譯時期的警告:
雖然泛型是如此的便利,但難免有缺點:
// 編譯出錯 List<int, int> array = new ArrayList<>();
public static <E> void foo(List<E> list){ // 編譯出錯 E element = new E(); list.add(element); }
public class Foo<T>{ // 編譯出錯 private static T field; }
instanceof
來確認參數類型public static <E> void foo(List<E> list){ // 編譯出錯 if(list instanceof ArrayList<Integer>){ } }
// 編譯出錯 List<String>[] strings = new ArrayList<>[2];
// 編譯出錯 public class FooException<T> extends Exception{ }
public class Example{ // 編譯出錯 public void print(Set<String> string){ } public void print(Set<Integer> integer){ } }