Java 中泛型 T 和 ? 的區別詳解

泛型中 T 類型變量 和 ? 通配符 區別

定義不同 :T 是類型變量,? 是通配符

使用范圍不同:

  • ? 通配符用作 參數類型、字段類型、局部變量類型,有時作為返回類型(但請避免這樣做)
  • T 用作 聲明類的類型參數、通用方法的類型參數 (這裡註意 類型參數 和 參數類型 是兩個概念)

通常我們使用 ? 的時候並並不知道也不關心這個時候的類型,這裡隻想使用其通用的方法,而且 ? 通配符是無法作用於聲明類的類型參數,一般作用於方法和參數上。而 類型變量 T 在類定義時具有更廣泛的應用。

在某些程度的使用上 ? 通配符與 T 參數類型是可以等效的,但是 T 參數類型並不支持下界限制 即 T super SomeTing 而 通配符支持 ? super SomeThing

如果你想寫一個通用的方法且該方法的邏輯不關心類型那麼就大膽的用 ? 通配符來進行適配和限制吧,如果你需要作用域類型(這可能在操作通用數組類型時更明顯)或者聲明類的類型參數時請使用 T 類型變量

類型參數定義瞭一種代表作用域類型的變量(例如,T),通配符隻是定義瞭一組可用於泛型類型的允許類型。通配符的意思是“在這裡使用任何類型”

在泛型的使用中我們經常可以看到這樣的用法:

public class Box<T> {
    // T stands for "Type"
    private T t;

    public void set(T t) { this.t = t; }
    public T get() { return t; }
}
List<? extends Integer> intList = new ArrayList<>();
List<? extends Number>  numList = intList;  // OK. List<? extends Integer> is a subtype of List<? extends Number>
public interface GenericProgressiveFutureListener<F extends ProgressiveFuture<?>> extends GenericFutureListener<F> {
    void operationProgressed(F future, long progress, long total) throws Exception;
}

如果你其用法和概念仍有疑問,那不妨繼續閱讀本文

瞭解他們的概念:Generic Types 和 Wildcards ,以及使用。

Generic Types 類型變量

通用類型即 T、F、K、V 這樣的寫法,它是一種是通過類型參數化的通用類或接口,也可以稱之為 類型變量

類型變量可以是任何非原始類型:任何類類型、任何接口類型、任何數組類型,甚至是另一個類型變量

按照慣例,類型參數名稱是單個大寫字母。

最常用的類型參數名稱是:

  • E – 元素(被 Java 集合框架廣泛使用)
  • K – 鍵
  • N – 數字
  • T – 類型
  • V – 值
  • S、U、V 等 – 第 2、3、4 種類型

用法

1.聲明通用的類型 – 泛型類:

當我們想對通用的對象類型進行操作時我們可能想到使用 Object ,但是使用 Object 在編譯時無法進行檢查,因為 Object 是所有類的父類,這可能導致我們意圖在傳入 Integer 並可以取出 Inerger 時,在另一部分代碼錯誤的傳入瞭 String

public class Box {
    private Object object;

    public void set(Object object) { this.object = object; }
    public Object get() { return object; }
}

為瞭避免上述的問題,我們可以選擇使用 類型變量

public class Box<T> {
    // T stands for "Type"
    private T t;

    public void set(T t) { this.t = t; }
    public T get() { return t; }
}

你也可以使用多個類型參數

public interface Pair<K, V> {
    public K getKey();
    public V getValue();
}

public class OrderedPair<K, V> implements Pair<K, V> {

    private K key;
    private V value;

    public OrderedPair(K key, V value) {
    this.key = key;
    this.value = value;
    }

    public K getKey()    { return key; }
    public V getValue() { return value; }
}

2.聲明通用的方法 – 泛型方法:

泛型方法 是引入自己的類型參數的方法。這類似於聲明泛型類型,但類型參數的范圍僅限於聲明它的方法。允許靜態和非靜態泛型方法,以及泛型類構造函數。

泛型方法的語法包括一個類型參數列表,在尖括號內,它出現在方法的返回類型之前。對於靜態泛型方法,類型參數部分必須出現在方法的返回類型之前。

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 static <T> void printListT(List<T> list) {
    for (Object elem : list)
        System.out.println(elem + " ");
    System.out.println();
}

一個完整的調用是

JestTestMain.<String>printListT(names);

但是通常可以省略類型,這裡使用到的功能是 類型推斷

JestTestMain.printListT(names);

有界類型參數

同時我們可以對類型參數進行限制通過 extends 關鍵字

如 <T extends Number> 這裡的泛型參數就限制瞭必須繼承於 Number 類。

public static <T extends Number> void printListT(List<T> list) {
    for (Object elem : list)
        System.out.println(elem + " ");
    System.out.println();
}

同時 Java 也支持多重限定,如 <T extends CharSequence & Comparable<T> & Serializable> 但是如果其中限定包含 類 需要寫在最前面

public static <T extends CharSequence & Comparable<T> & Serializable> void printListT(List<T> list) {
    for (Object elem : list)
        System.out.println(elem + " ");
    System.out.println();
}

Wildcards 通配符

通配符即指 ?

在泛型代碼中,? 表示未知類型。通配符可用於多種情況:

作為參數、字段或局部變量的類型,有時作為返回類型(但請避免這樣做)。

通配符從不用作泛型方法調用、泛型類實例創建或超類型的類型參數。

用法

通配符分為 3 種:

1.上界通配符:? extend 上界類型

List public static void process(List list) { /* ... */ }

我們可以使用上界通配符來放寬對變量的限制

2.無界通配符:?

如 List<?> 這表示未知類型的列表,一般有兩種情況下無界通配符是有用的:

  • 你正在編寫可以使用 Object類中提供的功能實現的方法
  • 當代碼使用不依賴於類型參數的泛型類中的方法時。例如,List.size或 List.clear。事實上,Class<?> 之所以如此常用,是因為 Class<T>中的大多數方法都不依賴於 T。

如何理解這句話的意思呢?來看一個例子:

public static void printList(List<Object> list) {
    for (Object elem : list)
        System.out.println(elem + " ");
    System.out.println();
}

printList 的意圖是想打印任何類型的列表,但是它沒有達到目標,其隻打印瞭 Object 實例的列表。它不能打印 List<Integer>、List<String>、List<Double>等,因為它們不是 List<Object> 的子類型。

編譯時將會報錯。

這裡我們換成通配符將正確運行

public class JestTestMain {
    public static void main(String[] args) {
        List<String> names= Lists.newArrayList();
        names.add("張三");
        names.add("張三1");
        names.add("張三2");
        printList(names);
    }

    public static void printList(List<?> list) {
        for (Object elem : list)
            System.out.println(elem + " ");
        System.out.println();
    }
}

打印:

張三 
張三1 
張三2 

這裡需要明白的一點是,List<Object> 和 List<?> 並不相同,你可以向 List<Object> 中插入 Object 對象,或者任何其子類對象,但是你隻能向 List<?> 中插入 null 值。

3.下界通配符:? super 子類

如:<? super Integer>

假設你要編寫一個將 Integer 對象放入列表的方法。為瞭最大限度地提高靈活性,希望該方法適用於 List<Integer>、List<Number>和 List<Object> 任何可以保存 Integer 值的東西。

public static void addNumbers(List<? super Integer> list) {
    for (int i = 1; i <= 10; i++) {
        list.add(i);
    }
}

類型擦除

我們要知道的一件事兒,編譯器在編譯時清除瞭所有類型參數,也就是說會將我們的類型參數進行實際的替換。

就算如此我們仍有使用泛型的理由

  • Java 編譯器在編譯時對泛型代碼執行更嚴格的類型檢查。
  • 泛型支持編程類型作為參數。
  • 泛型能夠實現泛型算法。

Java 語言中引入瞭泛型以在編譯時提供更嚴格的類型檢查並支持泛型編程。為瞭實現泛型,Java 編譯器將類型擦除應用於:

  • 如果類型參數是無界的,則將泛型類型中的所有類型參數替換為其邊界或 Object。因此,生成的字節碼僅包含普通的類、接口和方法。
  • 必要時插入類型轉換以保持類型安全。
  • 生成橋接方法以保留擴展泛型類型中的多態性。

類型擦除確保不會為參數化類型創建新類;因此,泛型不會產生運行時開銷。

下面舉兩個例子

// 類型擦除前
public class Pair<K, V> {

    public Pair(K key, V value) {
        this.key = key;
        this.value = value;
    }

    public K getKey(); { return key; }
    public V getValue(); { return value; }

    public void setKey(K key)     { this.key = key; }
    public void setValue(V value) { this.value = value; }

    private K key;
    private V value;
}
// 類型擦除後
public class Pair {

    public Pair(Object key, Object value) {
        this.key = key;
        this.value = value;
    }

    public Object getKey()   { return key; }
    public Object getValue() { return value; }

    public void setKey(Object key)     { this.key = key; }
    public void setValue(Object value) { this.value = value; }

    private Object key;
    private Object value;
}

// 類型擦除前
public static <T extends Comparable<T>> int findFirstGreaterThan(T[] at, T elem) {
    // ...
}
// 類型擦除後
public static int findFirstGreaterThan(Comparable[] at, Comparable elem) {
    // ...
}

最後感興趣的同學可以參考:

Java Tutorial on Generics
Generics in the Java programming language

到此這篇關於Java 中泛型 T 和 ? 的區別詳解的文章就介紹到這瞭,更多相關Java 泛型T和? 區別內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: