天天看點

java泛型深度解讀

簡介

泛型是Java SE 1.5的新特性,泛型的本質是參數化類型

( type parameters )

,也就是說所操作的資料類型被指定為一個參數,這種參數類型可以用在類、接口和方法的建立中.

在泛型類中定義參數化類型,在泛型表達式中,需要指定具體類型,即泛型在使用過程中将會被替換為具體的類型.

// 定義 參數類型
class ArrayList<E>
// 使用中 指定具體類型
ArrayList<String> list = new ArrayList<>();
           

原始類型(raw type)

: 就是去掉參數類型後的類,如示例中的

ArrayList

.

為什麼需要泛型

我們來看一個例子:

List list = new ArrayList();
        // 下列的添加方法完全沒問題
        list.add("one");
        list.add(1);

        // 取的時候, 如果你小心的,也沒問題
        // 需要強轉, 内部是以Object引用來存放
        String s = (String) list.get(0);
        int    i = (int) list.get(1);

        // 但是如果, 不小心在擷取時 類型判斷出錯的話
        for (int index = 0; index < list.size(); index++) {
            String str = (String) list.get(index);
            // index = 1時, 抛出java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
        }
           

上訴方式,有兩個問題.

  1. 由于java是靜态語言,應該盡量避免在一個容器數組中,添加不相幹的類型執行個體.否則可能引起類型轉換錯誤.
  2. 這種方式,沒有類型檢查,隻能夠在運作時候,系統抛出異常後,你才會發現錯誤.

接下來使用泛型:

List<Animal> list = new ArrayList<>();
        // 可以添加Animal及其子類
        list.add(new Animal());
        list.add(new Tiger());
        // 編譯器進行類型檢査,避免插人錯誤類型的對象
        // 編譯時期報錯,
        list.add("one");
           

可以看出,泛型隻允許添加 聲明的類及其子類,其他無關類無法加入到list中,并且嘗試将其他類型加入清單,将在編譯時直接報錯.

由此可以看出泛型的特點:

  1. 能夠對類型進行限定
  2. 在編譯期對類型進行檢查,編譯時報錯
  3. 對于擷取明确的限定類型,無需進行強制類型轉化
  4. 具有良好的可讀性和安全性

泛型類

一個泛型類 ( generic class ) 就是具有一個或多個類型變量的類.定義的變量用尖括号

<>

括起來,放在類名的後面.

public class Holder<T> {
    private T obj;
    public Holder(T t) {
        obj = t;
    }
    public void put(T t) {
        obj = t;
    }
    public T get() {
        return obj;
    }
}
           

泛型定義的類型變量,可以在 成員變量, 方法參數, 局部變量, 方法傳回值中使用.

要注意的是,靜态變量和靜态方法中,不能使用類中定義的泛型參數.

這裡的

T

可以代表任意類型(Object或其子類),需要注意的是,基本資料類型不能夠使用泛型,需要使用它們對應的包裝類(wrapper Class)

用具體的類型替換類型變量就可以執行個體化泛型類型,如

Holder<String> holder = new Holder<>();
           

泛型接口

泛型接口與泛型類對比差別别是,泛型接口中不能使用 類型參數作為成員變量.

泛型類的繼承

當父類為泛型類或者接口時,子類可以使用具體類型來繼承父類,也可以使用類型參數繼承父類

public interface Parent<T> {
    ...
// ======
// 使用具體類型來繼承父類
public class Son implements Parent<Animal> {
    ...
// ======
// 使用類型參數繼承父類
public class Son<E> implements Parent<E> {
    ...
           

但是要注意, 一個類不能實作同一個泛型接口的兩種變體,由于類型擦除的原因,這兩個變體會成為縣宮廷的接口

// Error
public class Son implements Parent<Animal> {
    ...
// ========= Error
public class Child extends Son implements Parent<String> {
    ...
           

這種方式,

Child

是實作了

Parent<Animal>

Parent<String>

,是不允許的.

泛型方法

除了泛型類,還可以聲明一個泛型方法. 泛型方法可以在泛型類中聲明,也可以在普通方法中聲明.

注意的是,靜态方法中,隻能使用方法中定義的類型參數,而不能使用泛型類中的類型參數.

// 普通類中的泛型方法
public class Normal {
    // 成員泛型方法
    public <E> String getString(E e) {
        return e.toString();
    }
    // 靜态泛型方法
    public static <V> void printString(V v) {
        System.out.println(v.toString());
    }
}
// 泛型類中的泛型方法
public class Generics<T> {
    // 成員泛型方法
    public <E> String getString(E e) {
        return e.toString();
    }
    // 靜态泛型方法
    public static <V> void printString(V v) {
        System.out.println(v.toString());
    }
}
           

一個原則:在能達到目的的情況下,盡量使用泛型方法。即,如果使用泛型方法可以取代将整個類泛化,那麼應該有限采用泛型方法。

泛型類中的參數類型和泛型方法中的參數類型,即使聲明為相同的類型參數,如T,兩者的類型不會互相影響,甚至可以說沒有任何關聯.方法中的類型,由傳入的參數決定,與泛型類的類型無關.

// 泛型類中的類型參數 用 T 表示
public class Holder<T> {
    ...
    // 成員泛型方法, 聲明的類型參數,也用 T 表示
    public <T> String getString(T t) {
        return t.toString();
    }
}
public static void main(String[] args) {
        // 泛型類為 Animal
        Holder<Animal> holder = new Holder<>(new Animal());
        // 泛型方法為 Vegetation
        String s = holder.getString(new Vegetation());
        System.out.println(s);
        // I'm Vegetation
    }
           

泛型方法的使用過程中,無需對類型進行聲明,它可以根據傳入的參數,自動判斷.

public class Main {
    public static void main(String[] args) {
        // 指定類型
        Main.<String>printString("one");
        // 不指定,自動推倒
        Main.printString("two");
    }
    static <T> void printString(T t) {
        System.out.println(t.toString());
    }
}
           

類型變量的限定

對于類型變量沒有限定的泛型類或方法, 它是預設繼承自

Object

,當沒有傳入具體類型時,它有的能力隻有

Object

類中的幾個預設方法實作.

如果我們要實作一個方法, 傳入兩個參數,傳回其中大的一個,即

max()

函數.

public static void main(String[] args) {
        // 傳入 4 , 2 , 自動裝箱成Integer類
        int r = max(4, 2);
    }
static <T> T max(T t1,T t2){
        // Cannot resolve method 'compareTo(T)'
        return t1.compareTo(t2) > 0 ? t1 : t2;
    }
           

如果沒有對類型進行限定,它預設隻有

Object

能力,它沒有

compareTo

方法,是以沒有比較能力,此時,即使在調用的時候傳入可以比較的對象,

max

方法會在編譯器報錯.

此時, 我們就需要對 類型參數進行限定,讓它能夠預設擁有一些類的"能力".

public static void main(String[] args) {
        // 傳入 4 , 2 , 自動裝箱成Integer類
        int r = max(4, 2);
        // r = 4
    }
    // 繼承 Comparable 的類具有比較功能,能夠比較大小 , 該函數傳回傳入的最大值
    static <T extends Comparable<T>> T max(T t1, T t2) {
        return t1.compareTo(t2) > 0 ? t1 : t2;
    }
           

從代碼中可以看出,

T

被限定為

Comparable

的子類(

Comparable

類本身是泛型類,也需要對他進行類型參數聲明,否則會引發編譯警告.),是以它擁有了 父類

Comparable

有的能力,即比較功能,這樣我們才能得到正确的結果.

類型參數的限定 可以記為

<T extends BoundingType>

,由于java有單繼承類多實作接口的特點,是以還可以有多個限定.

<T extends BoundingType1 & BoundingType2 & ...>

在 Java 的繼承中, 可以擁有多個接口超類型, 但限定中至多有一個類。 如果用 一個類作為限定, 它必須是限定清單中的第一個.

泛型的實作原理

java中泛型的實作是采用

類型擦除

的方式實作.

所謂的類型擦除,就是程式在編譯階段,編譯器會對泛型變量進行擦除(

erased

)操作,并替換為限定類型 (沒有限定的變量用 Object);泛型類也将擦除為原始類型; 在泛型表達式中(泛型的使用),會将類型替換為具體的類型(此時将發生強制轉換)

下面我們通過,反編譯泛型類的方式來揭開

類型擦除

的面紗.

使用

jad -sjava Holder.class

來反編譯

Holder

類.

// 泛型類
public class Holder<T> {
    private T obj;
    public void put(T t) {
        obj = t;
    }
    public T get() {
        return obj;
    }
}
// 泛型使用
public static void main(String[]args){
        Holder<Animal> holder=new Holder<>();
        holder.put(new Tiger());
        // 在使用過程中沒有發生強轉
        Animal animal=holder.get();
        }
// ---------------------------------
// 反編譯出來的類, 它的類型被擦除為Object
public class Holder {
    private Object obj;
    public Holder() {
    }
    public void put(Object t) {
        obj = t;
    }
    public Object get() {
        return obj;
    }
}
public static void main(String args[]){
        Holder holder=new Holder();
        holder.put(new Tiger());
        // 泛型的使用過程中,使用強制轉換為目标類型
        Animal animal=(Animal)holder.get();
        }
// -------------------------------------
// javap 反彙編對main方法,到處的指令碼
public static void main(java.lang.String[]);
    Code:
        0:new           #2                  // class generics/Holder
        3:dup
        4:invokespecial #3                  // Method generics/Holder."<init>":()V
        7:astore_1
        8:aload_1
        9:new           #4                  // class bean/Tiger
        12:dup
        13:invokespecial #5                  // Method bean/Tiger."<init>":()V
        16:invokevirtual #6                  // Method generics/Holder.put:(Ljava/lang/Object;)V
        19:aload_1
        20:invokevirtual #7                  // Method generics/Holder.get:()Ljava/lang/Object;
        23:checkcast     #8                  // class bean/Animal
        26:astore_2
        27:return
           

由上述反編譯的代碼可以看出,泛型類被擦除為 原始類型;

泛型類中的類型參數變量也擦除為

Object

類型;

泛型的表達式中,發生了強制轉換為目标類型.

從反彙編代碼中可以看出,

holder.get()

方法,被分解為兩條指令.

1. 原始類型調用方法,對應

invokevirtual

指令

2. Object類型強制轉換為Animal類型,對應

checkcast

如果限定為

T extends Animal

,則類型參數變量将被擦除為限定類型

Animal

// 泛型類
public class Holder<T extends Animal> {
    private T obj;
    ...
// 反編譯類
public class Holder {
    private Animal obj;
    ...
           

java泛型的局限

  1. 不能用基本類型執行個體化類型參數

泛型中的 類型參數在沒有限定的情況下 是預設 擦除為

Object

,而基本類型變量無法轉化為

Object

類型.

不過沒關系, java中8中基本類型,都有其對應的包裝類(Wrapper Class), 并且基本類型 使用參數傳遞是,将被 (自動裝箱)AutoBoxing 為包裝類型.

  1. 運作時,無法對類型參數進行檢查

由于編譯時,擦除了類型參數, 是以,所有的類型查詢隻産生原始類型.

是以,一下的語句是不可行的.

Holder<Tiger> holder = new Holder<>(new Tiger());
        // ERROR 無法對類型參數進行判斷
        if (holder instanceof Holder<Tiger>) ;
        if (holder instanceof Holder<T>) ;
           

由于類型擦除,

Holder<String>

Holder<Animal>

的執行個體,擷取的類都是原始類,是一樣的,是以他們的

getClass()

方法的傳回是一樣的.

  1. 不能直接建立參數化類型的數組

如這樣的代碼

Holder<Animal>[] holders = new Holder<Animal>[2]

,是通過不了編譯的.

但是,可以通過以下方式來建立數組,不會報錯,隻是受到警告

// 使用原始類型而後強制轉換
Holder<Animal>[] holders = (Holder<Animal>[]) new Holder[2];
// 使用通配符而後強制轉換
Holder<Animal>[] holders = (Holder<Animal>[]) new Holder<?>[2];
           
  1. 不能夠執行個體化類型變量

這樣的語句

T t = new T()

T t = new Object()

,是通過不了編譯的,一定要在泛型表達式中申明了具體類型,才能建立.

某些情況下,我們需要建立 參數類型的變量, 那麼前提是一定要知道被建立的類型.可以通過以下兩種方式來建立:

  • 反射建立
// 使用(如) newObject(Animal.class);
    static <T> T newObject(Class<T> cls) throw Exception{
        return cls.newInstance();
    }
           
  • jdk8以後,可使用構造器表達式
// 使用(如) newObject(Animal::new);
    static <T> T newObject(Supplier<T> constr) {
        return constr.get();
    }
           
  1. 不能構造泛型數組

不能直接執行個體化 類型數組,如

T[] arr = new T[2]

但是可以這樣

T[] arr = new Object[2]

, 原因是數組本身也有類型,用來監控存儲在虛拟機

中的數組,這個類型會被擦除為

Object

雖然這種方式能夠建立泛型數組,但是為了類型安全起見,最好提供構造器來實作泛型資料的建立.

// 使用構造器
    // 使用(如) newArray(Animal[]::new, 2);
    static <T> T[] newArray(IntFunction<T[]> constr, int length) {
        return constr.apply(length);
    }
    // 使用反射
    // 使用(如) newArray(String.class, 2);
    static <T> T[] newArray(Class<T> cls, int length) throws Exception {
        return (T[]) Array.newInstance(cls, length);
    }
           
  1. 不能在靜态變量和靜态方法中,使用泛型類中的類型參數

如以下的方式都不允許

public class Test<T> {
    // Error
    private static T t;
    // Error
    public static T test() {
        T t;
    }
}
           
  1. 注意擦除後的沖突
  • 由于類型擦除,方法重寫時,會下列沖突.
public class Holder<T> {
    public boolean equals(T t) {
        ...
    }
           

由于類型擦除,該方法會被擦除為

boolean equals(Object t)

,這和Object類中的

equals

方法完全沖突了,傳回值和方法名,參數都一緻了.

這種沖突,解決方案隻能是,将方法重命名!

  • 由于類型擦除,方法重載時,也可能發生沖突.

再觀察下面的代碼:

public interface Parent<T> {
    T get();
}
public class Son implements Parent<Animal> {
    @Override
    public Animal get() {
        return new Animal();
    }
}
public class Main {
    public static void main(String[] args) {
        Parent<Animal> parent = new Son();
        Animal animal = parent.get();
    }
}
           

父類中根據類型擦除, 擁有

Object get()

方法, 子類傳入具體的類型參數,擁有

Animal get()

方法,且繼承父類方法,是以子類中同時擁有這兩個方法.

這兩個方法,方法參數相同,就隻有傳回值類型不一樣.

在java文法中,是不允許這樣的兩個方法同時在一個類中存在,會把他們認為是同一種方法(方法簽名根據方法參數和方法名來确定).

但是jvm卻能分辨,jvm的方法簽名,是通過方法參數,方法傳回值,方法名來确定的,是以jvm允許這樣的方法存在, 是以,對于這種沖突,不需要我們自己來處理, jvm通過一種稱為

Bridge Method

的方式來實作這種方式下的多态調用沖突.

感興趣的可以檢視,筆者的另一篇文章

java中多态的實作原理

為什麼使用 類型擦除來實作泛型

因為,泛型提出來時,已經是java1.5的版本,java已經經曆過10年的發展,java遺留的代碼量可想而知.

為了相容這部分的(舊)代碼,而不得不采用這種方式來實作.

通配符 <?> <? extends T> <? super T>

泛型通配符解釋起來比較複雜,這裡就不進行展開,感興趣的可以檢視筆者的另一篇文章

java泛型通配符詳解及實踐

引用

  1. java核心技術 卷1(第10版)