【從基礎學 Java】泛型

引言

在面向對象的世界裏,咱們若是須要一個容器來盛裝對象。舉個例子:一個籃子。咱們能夠用這個籃子裝蘋果,也能夠用這個籃子裝香蕉。基於 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

  • E:表示一個元素,Java 集合框架中使用最多
  • K:鍵
  • N:數字
  • T:類型
  • V:值
  • S,U,V:其它類型

原始類型(raw type)

原始類型指的是,不包括泛型參數的類型,如上述泛型類中的 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>){
    // 這裏作邏輯處理
}

無界限通配符

使用無界限通配符表示不肯定的類型,如下兩種狀況可使用無界限通配符:

  • 當方法的參數能夠用 Object 對象替換
  • 方法的實現不依賴具體的類型

好比,有一個打印集合對象的方法:

// 定義一個打印集合對象列表的方法
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<?> 只能插入 nullList<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
  • 入參能夠用 Object 代替的,使用無邊通配符
  • 須要獲取入參和出參的變量,不要使用通配符

這種原則也叫作 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)和參數化類型。

使用非具體化類型作可變參數的潛在缺陷

當使用可變參數做爲泛型輸入參數時,會形成堆污染。如:
能夠經過以下註解消除編譯時期的警告:

  • @SafeVarargs
  • @SuppressWarnings({"unchecked", "varargs"})

泛型的限制

雖然泛型是如此的便利,但難免有缺點:

  • 不能用基本類型實例化類型參數
// 編譯出錯
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){
    }
}
  • 運行時類型查詢只適用於原始類型
  • Varargs 警告
  • 泛型類的靜態上下文的類型變量無效
相關文章
相關標籤/搜索