深入了解 Java 泛型:從基本概念到實踐應用 | 3 種泛型 | Java、C++、C# 泛型概念

深入了解 Java 泛型:從基本概念到實踐應用 | 3 種泛型 | Java、C++、C# 泛型概念

Overview of Content

Java 泛型是一種強大的工具,為程式開發提供了更多的靈活性和安全性。在這篇文章中,我們將深入探討為何需要泛型及其帶來的諸多優點,並解析泛型的起源和原理。此外,我們還會討論泛型可能帶來的副作用及其應對策略。接著,我們將介紹如何在類、界面和方法中使用泛型,並探討如何使用限定類型來實現更精確的類型控制。

通配符是泛型的一個重要部分,我們會詳細解釋其在泛型類繼承和函數引數中的應用,並比較通配符與限定類型泛型的差異;最終,我們將深入探討 JVM 如何實現泛型,包括 C++ 模板、C# 泛型的類型膨脹以及 Java 的偽泛型概念

此外,還會介紹 Java 泛型擦除和橋接函數的工作原理,並提醒在使用泛型時需要注意的問題,例如 Heap Pollution

這篇文章旨在為讀者提供一個全面且深入的 Java 泛型指南,無論是初學者還是有經驗的開發者,都能從中獲得有價值的見解和實踐建議

寫文章分享不易,如有引用參考請詳註出處,如有指導、意見歡迎留言(如果覺得寫得好也請給我一些支持),感謝 😀

個人程式分享時比較注重「縮排」,所以可能不適合手機的排版閱讀,建議切換至「電腦版」、「平板版」視窗看

Generic for what:為何需要泛型

對於相同的運算,或是流程不用重複寫相同的程式,以下程式可看到,明明都是加法的程式,不過就因為「類型(type)」不同就需要做多次相同程式的撰寫… 而使用泛型(Generic)就可以達到相同的目的,只撰寫一次程式(說繞口一點就是「參數化類型」)


public int sumOfTwoInt(int a, int b) {
    return a + b;
}

public float sumOfTwoFloat(float a, float b) {
    return a + b;
}

泛型優點

● 如果有定義泛型就不用強制轉換,它會更加有效的使用「靜態程式的檢查特性」!讓產出的程式更加穩定,提早在「編譯期」就發現問題

void enhanceLang() {
    List<String> messages = new ArrayList<>();

    messages.add("Hello");
    messages.add("World");
    messages.add(1000);           // 錯誤類型,編譯期就會發生錯誤

}

對於不同類型(Type)的數據,也可以用相同的程式處理,達到很好的兼容效果

泛型如果未定義類型,是可以任意添加其他類型的值,Java 並不會檢查,範例如下

// 未定義泛型時

List list = new ArrayList();
list.add("Hello");
list.add("World");
list.add(123);

將一個對象加入集合中,如果未定義類型就不會檢查其類型,取出時預設為 Object 類型

此時強制轉型會拋出異常,ClassCastExecption 強轉型錯誤

泛型的來源、原理

JDK 5 後 Java 才加入泛型,往往有人稱 Java 的泛型為「偽泛型」,這其實是 為了向下兼容;另外在 JVM 虛擬機是不支持泛型的,所以 Java 語言為了實現泛型使用了 偽泛型

Java 會在 編譯期間擦除 所有的泛型訊息,這樣就可以讓 JVM 使用相同的字節碼而不必新增,在 JVM Runtime 時就不存在所謂的泛型訊息 (後面會介紹泛型擦除)

● 由於泛型擦除的存在,Java 的泛型是在編譯期間處理的,而不是在運行時… 這意味著在 JVM 運行時,不存在泛型類型的信息,所有的類型檢查和轉換都在編譯期間完成。這樣做的好處是可以簡化 JVM 的設計,並且確保向下兼容

運行時類型轉換

編譯器會在必要的地方插入類型轉換,以確保類型安全。這些轉換是在「運行時, Runtime」執行的,但它們是基於編譯期間插入的轉換操作

Java 泛型副作用

不能使用基本數據類型 (byte、char、short、int、long、double),因為編譯過後會經過擦除,擦除後後會轉換為 Object 類型,在轉換回特定類型時無法得知「目標類的類資訊」

不能使用 instanceof 運算符號判斷類型,同樣是因為類型擦除後所帶來的副作用,會導致無法判別類型(Type

擦拭後都是 Object 類型,沒得判斷

泛型類」不能使用在靜態:因為泛型創建物件時才能確定,但靜態方法、靜態參數,是不用加載就可以使用 (泛型方法則可以,因為泛型方法是呼叫後才加載)

泛型可能會導致多載方法衝突,同樣是因為擦除後會轉為 Object 類,導致重複定義

// 泛型
public void HelloWorld(T t) {        // 編譯字節碼後,同 Object t
    ...
}

public void HelloWorld(Object t) {      // 編譯後,泛型擦除後的結果
    ...
}

無法創建泛型對象,但可以使用反射創建泛型對象 (因為反射就是 Runtime 運行)

public static <E> void yo1(E e) {
    E elements = new E(); // Error
}

public static <E> void yo2(E e, Class<E> clz) {
    E elements = clz.newInstance();
}

● Java 中沒有所謂的「泛型數組,同樣也是因為擦除,無法判斷是否是同一類型(不安全)

// 非法:不能創建泛型數組
List<String>[] stringLists = new List<String>[10];

泛型類、界面、方法

泛型的本質是為了參數化類型,也就是指定參數類型,把類型當成引數一樣傳遞

● 在泛型的使用中,操作數據類型被指定為一參數,這參數可被用在 Classinterfacemethod

泛型允許多個變量(多個參數類型),引入一個類型變數 T (其他字母也可以 T, E, V, K, Params 等等,重點是「正確表達出泛型的含義」) 並用 <> 包住

Generic Class:泛型類

● 泛型類是以類(Class)為基礎再加上泛型類型的符號就可以使用,之後這個類就可以使用泛型指定的類型

class Clz<T> {
    private T data;
    
    public Clz(T data) {
        this.data = data;
    }
}

泛型類是以類(Class)為基礎再加上泛型類型的符號就可以使用,之後這個類就可以使用泛型指定的類型

public class GenericClass1<T> {
    private T data;

    private GenericClass1() {
    }

    public GenericClass1(T data) {
        this();    // 呼叫無參構造函數
        this.data = data;
    }

    public T GetData() {
        return data;
    }
}

// 兩個泛型變量 T K...
public class GenericClass2<T, K> {

    private T t;
    private K k;

    public GenericClass2() {
    }

    public void setDataT(T t, K k) {
        this.t = t;
        this.k = k;
    }

    public T getTData() {
        return t;
    }

    public K getKData() {
        return k;
    }
}

泛型類(Generic Class)的繼承規則:泛型類可以繼承或是拓展其他泛型類;像是以下範例,NormalClass 實作泛型類 MyClz

之後的小節會再說明更多

abstract class MyClz<K> {
    
    public abstract int compareTo(K k);
    
}

class NormalClass<T, K> extends MyClz<K> {
    private T data;

    public NormalClass(T data) {
        this.data = data;
    }

    @Override
    public int compareTo(K k) {
        return 0;
    }
}

Generic Interface:泛型界面

● 泛型界面基本上跟 Class 相同,不過泛型界面是使用界面(interface)作為基礎,再加上泛型類型的符號,之後就可以使用泛型界面

interface GInterface<T> {

    T getData();

}

以下範例使用,兩種實現方式來使用泛型界面,分別是 1. 實作界面時未指定類型(將指定類型的責任往使用該類者轉移),2. 實作界面後指定類型(泛型類同樣也可以這樣使用)

// 泛型界面
public interface GenericInterface<T> {
    public void setData(T t);
    public T getData();
}

// 實現 1 未指定類型
public class NormalClass<T> implements GenericInterface<T> {
    public T data;

    public void setData(T t) {
        this.data = t;
    }

    public T getData() {
        return data;
    }
}

// 實現 2 指定類型
public class NormalClass2 implements GenericInterface<String> { 
    public String data;


    public void setData(String t) {
        this.data = t;
    }

    public String getData() {
        return data;
    }
}

public static void main(String[] args) {
    NormalClass<String> n = new NormalClass<>();
    n.setData("Hello World");
    n.getData();

    NormalClass2 n2 = new NormalClass2();
    n2.setData("Hello World");
    n2.getData();
}

Generic Method:泛型方法

● Generic Method 可使用在任何場景的類中… 包括泛型類、普通類(也就是不單指定泛型類)

// 泛型類型
class GenericClz<T> {
    // 泛型方法
    void setData(T data) { 
        // TODO:
    }
}


// 普通類
class NormalClz {
    // 泛型方法
    <T> void setData() { }
}

● 「混和型」也可以使用 Generic Method,可載入不同於「類」定義時的類型… 範例如下:


public static void main(String[] args) {

    NormalClass n1 = new NormalClass();
    String s1 = n1.<String>GetGeneric("Hello", "World", "123");
    System.out.println(s1);
    int i1 = n1.<Integer>GetGeneric(111, 222, 333);
    System.out.println(i1);

    // 原本定義為 String 型態的變數 T (內部就是 String 型態)
    NormalClass2<String> n2 = new NormalClass2<>();
    // 泛型方法可分開定義,自己定義方法內部使用的 T 為 Double 類型
    double i2 = n2.<Double>GetGeneric(0.0, 0.1, 0.2, 0.3, 0.4, 0.5);
    System.out.println(i2);
}

// 1. 一般類別使用 Generic Method
public class NormalClass {
    // <T> T,<T> 為返回的型態 T 為返回的值
    public <T> T GetGeneric(T ...a) {
        return a[a.length / 2];
    }
}

// 2. 混和 Generic Class interface、method
public class NormalClass2<T> implements GenericInterface<T> { 
    public T data;

    @Override
    public void setData(T t) {
        this.data = t;
    }

    public <T> T GetGeneric(T ...a) {
        // 內部的類型 T != 外部的類型 T
        return a[a.length / 2];
    }
}

限定類型 Qulified Type

有時會對類型變量進型約束:例如約束型態 T 必須要實現 Comparable 界面

如果不進行約束將無法確保該型態是否可執行此界面的函數!

// 未用現定符號,「無法」確定是否都有實現 compareTo 方法,這時就需要限定類型 Qulified Type

public static <T> T min(T a, T b) {
    return (a.compareTo(b) > 0) ? a : b;
}

Constraint (約束)、limitation (限制)

● 基礎限定方法 <T exetnds A>,其中 extends 左右都允許多個泛型符號,像是… <T, k extends A & B>

注意:限定類型中 只能有一類 (一個繼承),而且必須是第一個 !

● 可限定多個類型,使用 & 連接多個限制條件 <T extends A & b>,當然如上面所說,第一個 A 是「類別」,之後的 b 則是「界面」,也就是同樣遵守著 Java 語言的單一繼承的特性!

// 1. 使用限定符號
public static <T extends Comparable> T minNumber(T a, T b) {
    return (a.compareTo(b) > 0) ? a : b;
}

// 2. 多個限定
public static <T extends Comparable & Serializable> T maxNumber(T a, T b) {
    return (a.compareTo(b) > 0) ? b : a;
}

// 3. 類限定類 Activity為類型,Serializable為界面
public static <T extends Activity & Serializable> T recordActivity(T a) {
    Log.d("TEG", a.toString);
    return a;
}

● 想看更多的範例,可參考 Java 源碼中實現的 List and ArrayList 類,這些類型都有使用泛型約束…


Universal Symbol 通配符

Java 的泛型有另外一個概念「通配符號, Universal Symbol」,通配符會使用「問號 ?」來呈現

它有兩種拓展使用方式:extendssuper ,它們個代表了不同的限制,而在了解通配符之前要先了解,為何要使用它 ? 這就要先了解 繼承 & 泛型的關係

泛型類繼承

泛型類繼承

1. 泛型雖然在編譯過後在字節碼(Byte Code)被擦除,但是仍會在檢查的時候用到,只要 泛型類型的原型基類相同,就符合泛型類繼承

2. 如果與基類不同就無法實現泛型類繼承… 簡單範例如下

public class Extends2 {

    public static void main(String[] args) {
        //"1. "    OK
        Clothes<UNIQLO> c1 = new T_shirt<>();
        Clothes<UNIQLO> c2 = new Colorful_T_shirt<String, UNIQLO>();
        Clothes<UNIQLO> c3 = new LongPants<Integer, UNIQLO>();

        //"2. "    ERROR, 基類為 Clothes<UNIQLO>,與 Clothes<String> 不同
        Clothes<String> c4 = new Colorful_T_shirt<String, UNIQLO>();
    }
}

class UNIQLO {
}

interface Clothes<T> {
}

class T_shirt<T> implements Clothes<T> {	
}

class Colorful_T_shirt<E, T> extends T_shirt<T> {	
}

class Pants<T> implements Clothes<T> {
}

class LongPants<K, T> extends Pants<T> {
}

class ShortPants<K, T> extends Pants<T> {
}

通配符功能:泛型類之間的關係

● 要了解通配符能為泛型帶來什麼樣的功能,先要了解 泛型類之間的關係,在以下範例中我們來觀察一下,泛型是否可以判斷「繼承」的特性?

public class Extends {

    public static void main(String[] args) {
        // 泛型類未指定類型
        Shop s1 = new Shop();
        Shop s2 = new Shop();
        //"1. "
        s2 = s1;    // ok, but not good

        Shop<Food> foodShop = new Shop<>();
        Shop<Fruit> fruitShop = new Shop<>();
        Shop<Meat> meatShop = new Shop<>();

        //"2. "
        foodShop = fruitShop;        // Err        
        foodShop = meatShop;         // Err
        //"3. "
        foodShop = (Shop<Food>)meatShop;         // Err
    }

}

class Shop<T> {
    private T t;

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

    T get() {
        return t;
    }
}

class Food {
}

class Meat extends Food {
}

class Beef extends Meat {
}

class Fruit extends Food {
}

class Banana extends Fruit {
}

A. 在一般類時可賦予值,所以 ok(但是不夠好,無法善用靜態語言的檢查特性)

B. 無法使用的原因是它規定了傳入的參數必須是 Shop<Fruit>雖然一般的繼承有關係,但是到了泛型類別時它無法自動判斷是否有關係,它們全都繼承於 Object 類,導致編譯器無法判斷兩個類之間的關係 (因為 Shop 才是主體,泛型會被擦除,所以繼承於 Object)

C. 同樣的,也 無法強制轉型,Java 不允許這樣的操作,會提醒我們 Inconvertible types; cannot cast 錯誤

● 從這個例子中,可以引出 讓普通繼承 & 泛型產生關係就要使用通配符

使用通配符:宣告、引數

● 若泛型使用了 通配符 ?(單單只有通配符),使用在「宣告」時,那就不能設定也不能取值目的是為了 類型檢查… 範例如下

public class Extends {

    public static void main(String[] args) {
        test();
    }

    private static void test() {
        Shop<?> shop = new Shop<>();
        // 無法確定確切的類
        //shop.set(new Banana());		
        //shop.set(new Fruit());
        //shop.set(new Food());
        shop.set(null);    // null 可以

        // 無法確定取出的類
        //Food d = shop.get();
        //Fruit f = shop.get();
        //Banana b1 = shop.get();	
        Banana b2 = (Banana) shop.get();// 強制轉型,可能有問題 warning
        Object o = shop.get();	// 所有類的父類 Object 一定是
    }
}

class Shop<T> {
    private T t;

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

    T get() {
        return t;
    }
}

class Food {	
}

class Meat extends Food {
}

class Beef extends Meat {
}

class Fruit extends Food {
}

class Banana extends Fruit {
}

● 若泛型使用了 通配符 ?(單單只有通配符),但是作為「引數」,代表 全部類型都可以接受

public class WildcardExample {

    // 定義一個方法,接受通配符類型的 List 作為引數
    public static void printList(List<?> list) {
        for (Object obj : list) {
            System.out.println(obj);
        }
    }

    public static void main(String[] args) {
        // 創建一個 List<String>
        List<String> stringList = new ArrayList<>();
        stringList.add("Hello");
        stringList.add("World");

        // 創建一個 List<Integer>
        List<Integer> intList = new ArrayList<>();
        intList.add(1);
        intList.add(2);

        // 調用 printList 方法,傳入不同類型的 List
        System.out.println("Printing stringList:");
        printList(stringList);

        System.out.println("Printing intList:");
        printList(intList);
    }
}

通配符 & extends

通配符與 extends 配合後的功能是:安全的 取得 (get) 數據作為引數,基礎使用 <? extends X>,表示類型的 上限到 X 類,包含 X 類以及其衍生子類


public static void main(String[] args) {
    funcPrint_2(shop1);        // ok
    "1: " funcPrint_2(shop2);  // ok

    //Shop<T> 是泛型
    Shop<? extends Fruit> shop3 = new Shop<>();
    "2: " Fruit f1 = shop3.getType();
    "3: " Apple a1 = (Apple) shop3.getType();
    Apple apple = new Apple();
    "4: " shop3.setType();     // Fail !!
}

static void funcPrint_1(Shop<Fruit> p) {
    System.out.println("Func1: " + p.getType().type());
}

static void funcPrint_2(Shop<? extends Fruit> p) {
    System.out.println("Func2: " + p.getType().type());    // 安全取得
    p.set(new Fruit()); // Fail !!
}

A. 使用 <? extends Fruit> 拓展了傳入泛型類別的寬度,拓展範圍是 Fruit 跟它的子類 (include Fruit)

B. 可安全的取得類,因為返回的一定是 Fruit 類 (上限到 Fruit)

C. 返回的是 Fruit 類,它的 子類要強制轉型

D. 只知道設定的是 apple,但卻不能具體的知道 Shop 內的類型變數 T 要設定哪個類 (可能是 Fruit、Apple、HonFuShi、Orang)

主要用來安全的訪問(取得)數據,就像是 Kotlin 的協變 (out)

● 範例二:強調通配符 extends 是限定泛型,extends 可安全的取出


public class Extends {

    public static void main(String[] args) {    
        testExtends();
    }

    private static void testExtends() {
        Shop<? extends Fruit> shop = new Shop<>();	// extends 用於安全取值
        //shop.set(new Banana());	// 無法確定確切的類,有可能是 Fruit、Banana or ...
        //shop.set(new Fruit());
        //shop.set(new Food());
        shop.set(null);    // null 可以

        Food d = shop.get();
        Fruit f = shop.get();        // 只能肯定一定是 Fruit
        //Banana b1 = shop.get();	// 只能取出是 Fruit 的基類
        Banana b2 = (Banana) shop.get();
        Object o = shop.get();
    }    
}


class Shop<T> {
    private T t;

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

    T get() {
        return t;
    }
}

class Food {	
}

class Meat extends Food {
}

class Beef extends Meat {
}

class Fruit extends Food {
}

class Banana extends Fruit {
}

通配符 & super

通配符與 super 配合的功能是:安全的 設定 數據作為引數,基礎使用 <? super X>,表示類型的 下限是 X 類,包含 X 類 以及其父類(超類)

限定符不能使用 super像是如果使用 <T super Shop> 語法錯誤


public static void main(String[] args) {

    Shop<Food> shop1_1 = new Shop<>();
    Shop<Fruit> shop2_1 = new Shop<>();
    Shop<Apple> shop3_1 = new Shop<>();
    Shop<Orange> shop3_2 = new Shop<>();
    Shop<HonFuShi> shop4_1 = new Shop<>();

    funcSuperPrint(shop1_1);    // ok    (基類
    funcSuperPrint(shop2_1);    // ok    (包含限定界線
    "1: "
    funcSuperPrint(shop3_1);	// fail
    funcSuperPrint(shop3_2);	// fail
    funcSuperPrint(shop4_1);	// fail

    Shop<? super Fruit> test = new Shop<>();
    "2: "
    test.setType(new Food());	// fail
    test.setType(new Fruit());  // ok
    test.setType(new Apple());  // ok
    "3: " Object o = test.getType();
}

public static void funcSuperPrint(Shop<? super Fruit> p) {
    System.out.print("Super Func: " + p.getType());
    
    // fail 返回的是 Object 類
    "4: "System.out.println("Super Func: " + p.getType().type());	
}

A. 使用 <? super Fruit> 拓展了傳入泛型類別的寬度,拓展範圍是 Fruit 跟它的父類(include Fruit),所以不符合入參拓展規定

B. 編譯器不知道set 它的具體類型,但可保證 Fruit and 其 子類 可安全轉型成 Fruit,set 無法得知設定的超類是否能轉型成 Fruit

C. 返回的類別是 Object,無法得知回傳的具體類別(有可能是 Food、Fruit),但一定是 Object 的子類

D. 返回 Object 類別,除非強轉否則無法使用指定的子類 Function

主要用來安全的設定 (set) 數據,只可寫入 X 的子類,就像是 Kotlin 的逆變 (in)

● 範例二:集合的安全設定 (set) 就可以使用到 通配符 + super


public class Extends {

    public static void main(String[] args) {
    }

    private static void testSuper() {
        // 安全設定 Fruit 基類數據
        Shop<? super Fruit> shop = new Shop<>();
        // 能確定一定是 Fruit 的衍生子類,要嘛 Fruit 要嘛 Fruit 的子類
        shop.set(new Banana());	
        shop.set(new Fruit());
        
        //shop.set(new Food());	// Err 無法確定跟 Fruit 的關係
        shop.set(null);    // null 可以

        // 無法確定取出的類
        //Food d = shop.get();
        //Fruit f = shop.get();
        //Banana b1 = shop.get();	
        Banana b2 = (Banana) shop.get();
        // 只能確定取出的一定是 Fruit 的基類,而 Object 則一定 Ok
        Object o = shop.get();	
    }
}

class Shop<T> {
    private T t;

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

    T get() {
        return t;
    }
}

class Food {	
}

class Meat extends Food {
}

class Beef extends Meat {
}

class Fruit extends Food {
}

class Banana extends Fruit {
}

● 為何安全設定數據,但無法安全取得數據 ?

Get 時無法確定是哪一個類,主要原因是因為泛型限制丟失,只能用 Object 存放

super & extends 關係 & 使用

Shop<?> 非限定通配符,等同於 Shop<\? extends Object>

Shop<? extends T>(上界)、Shop<? super T>(下屆) 兩者為 限定通配符


class Shop<T> {
    private T t;

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

    T get() {
        return t;
    }
}

class Food {	
}

class Meat extends Food {
}

class Beef extends Meat {
}

class Fruit extends Food {
}

class Banana extends Fruit {
}

配合上面的程式看下面限定 & 非限定通配符的關係圖

Java Collections 的通配符使用

● Java Collections 內大量地使用了泛型 super & extends,以 copy 為例

使用了 super 安全的設定 T 的衍生類數據,使用 extends 安全的取得了 T 的衍生類數據,以此做為 copy 基礎

public static <T> void copy(List<? super T> dest,     // 安全 Set
                            List<? extends T> src) {    // 安全 Get
        int srcSize = src.size();
        if (srcSize > dest.size())
            throw new IndexOutOfBoundsException("Source does not fit in dest");

        if (srcSize < COPY_THRESHOLD ||
            ...
        } else {
            // 取得 Iterator
            ListIterator<? super T> di=dest.listIterator();
            ListIterator<? extends T> si=src.listIterator();
            for (int i=0; i<srcSize; i++) {
                di.next();
                di.set(
                    si.next()    // 安全取得
                );    // 1. 安全設定
            }
        }
    }

● 而為何要這樣使用 extends & super,可以看下面的例子

A. 類關係


class PC {
    int id;
    PC(int id){
        this.id = id;
    }
}

class CPU extends PC {
    int id;
    CPU(int id){
        super(id);
        this.id = id;
    }
}

B. Collections 的 copy 函數使用


// 指定
public static void copy_1(List<CPU> p1, List<CPU> p2) {
    Collections.copy(p1, p2);
}

// 限定類型
public static <T> void copy_2(List<T> p1, List<T> p2) {
    Collections.copy(p1, p2);
}

// 限定 super (set)
public static <T> void copy_3(List<? super T> p1, List<T> p2) {
    Collections.copy(p1, p2);
}

// 限定 super (set)、extends (get)
public static <T> void copy_4(List<? super T> p1, List<? extends T> p2) {
    Collections.copy(p1, p2);
}

C. 測試


public class UseMixSuperExtend {

    public static void main(String[] args) {
        fixed();
        System.out.println("");
        gMethod();
        System.out.println("");
        Mix_1();
        System.out.println("");
        Mix_2();
    }

    // 固定 (指定) 泛型
    public static void fixed() {
        List<CPU> src = new ArrayList<>();
        src.add(new CPU(1));
        List<CPU> dest = new ArrayList<>();
        dest.add(new CPU(2));

        System.out.println("cpu-cpu before copy: " + dest.get(0).id);
        copy_1(dest, src);    // src 複製到 dest
        System.out.println("cpu-cpu after copy: " + dest.get(0).id);
    }

    // 泛型方法
    public static void gMethod() {
        // 使用基類 PC
        List<PC> src = new ArrayList<>();
        src.add(new PC(1));
        // 使用基類 PC
        List<PC> dest = new ArrayList<>();
        dest.add(new PC(2));

        System.out.println("PC-PC before copy: " + dest.get(0).id);
        
        UseMixSuperExtend.<PC>copy_2(dest, src);					// 轉為 PC 泛型
        
        System.out.println("PC-PC after copy: " + dest.get(0).id);
    }

    public static void Mix_1() {
        // 指定衍生類 CPU
        List<CPU> src = new ArrayList<>();
        src.add(new CPU(1));
        // 使用基類 PC
        List<PC> dest = new ArrayList<>();
        dest.add(new PC(2));

        System.out.println("cpu-PC<CPU> before copy: " + dest.get(0).id);
        
        // // 使用泛型方法 Err,因為第一個參數不符合
        // UseMixSuperExtend.<PC>copy_2(dest, src); 
        
        // 使用通配符轉型為 CPU
        UseMixSuperExtend.<CPU>copy_3(dest,     // <? super CPU>
                                      src);
        
        System.out.println("cpu-PC<CPU> after copy: " + dest.get(0).id);
    }

    public static void Mix_2() {
        // 指定衍生類 CPU
        List<CPU> src = new ArrayList<>();
        src.add(new CPU(1));
        // 使用基類 PC
        List<PC> dest = new ArrayList<>();
        dest.add(new CPU(2));

        System.out.println("cpu-PC<PC> before copy: " + dest.get(0).id);
        
        // 使用通配符轉型為 PC Err,因為第二個參數是 CPU 
        //UseMixSuperExtend.<PC>copy_3(dest, src);	
        
        UseMixSuperExtend.<PC>copy_4(dest,      // <? super PC>
                                     src);     // <? extends PC>
        
        System.out.println("cpu-PC<PC> after copy: " + dest.get(0).id);
    }


}

Mix_1 & Mix_2 的差別在於,使用不同的泛型方法類型,但 Mix_2 更加完善

通配符 vs. 限定類型泛型

● 限定類型的泛型 T 必須是具體的某一種類,會有類型之間的轉換問題

通配符 ? 讓泛型轉型更靈活… 但 反射可以打破通配符的限制

類似於限定類別 <T extends X>,但限定類別用於限定泛型的類型

通配符 <? extends X> 用於,限定/拓展 Func 接收的參數,並給予它限制

限定符類型範例
非限定通配符Plate<?> or Plate<? extends Object>
限定通配符(指定類型)Plate<? extends Fruit> or Plate<? super Fruit>

虛擬機是如何實現泛型

最早是出現在 C 的模板(Temple),Java 只能通過 Object 是所以類的父類、強轉兩個特點型成泛型,Object 可轉型成任何類,只有在運行期才會知道是什麼類,編譯期無法檢察 Object 是否轉型成功,因此有許多的 ClassCastException 風險會轉駕到執行期

C++ 模板

● C++ 的實現會有真正的泛型

泛型最早是出現在 C++ 的 模板(Temple) 設計


template<typename t>
class TemplateStack {
private:
    enum {MAX = 10};
    int index;
    T List[MAX];
public:
    TemplateStack() : index(0) {}

    bool isEmpty() const;
    bool isFull() const;

    bool push(T t);
    bool pop(T& t);

    virtual ~TemplateStack() {};
};

C# 泛型:類型膨脹

● C# 裡面的泛型無論在程式源碼中,編譯後的程式 IL(Intermediate Language 中間語言,這時候泛型是一個佔位符),或 Running time CLR (Common Language Runtime 通用運行語言CLR) List<int>、List<String> 是兩種不同類型!都有自己的虛擬方法表,這種實現稱為類型膨脹,基於這種方法型成的泛型為 真實泛型

Java 偽泛型概念

Java / kotlin 虛擬機其實內部並無泛型,並不是真正的泛型 ,JDK 5 才有泛型,為了向上兼容,才會演變成 偽泛型

在編譯後的字節碼中以替換成原生類型(Raw Type),並在相應的第方插入強制轉型代碼,所以對於「運行期」的 Java 語言來說 List<Integer>List<String> 就是同一類別

Java 語言中的泛型實現方法稱為 類型擦除,基於這種方法實現的泛型是偽泛型

在泛型中並不是所有的擦除都會變成 Object 的,當泛型有限制性,Test<T extends Apple>,則會被擦除成 Apple


List list = new ArrayList<Integer\>();
List<String> list2 = list;

擦除過後 ArrayList<Integer> 轉為 Array List,List<String> 轉為 List


Java 泛型擦除:橋接函數

● 在編譯成字節碼後,泛型會擦除,如果沒有限定會變成 Object,如果有限定則會擦除成限定類

/**
 * 源碼 `.java` 檔
 */ 
public interface Plate<T> {
    void set(T t);
    T get();
}

/**
 * 擦除後 `.class` 檔
 */
public interface Plate {
    void set(Object t);    // 轉換為 Object
    Object get();
}

● 在擦除完後,當有需要時就會新增一個橋接函數,調用時用「橋接函數」,並且強制轉型再呼叫轉型的方法

生成橋方法的目的也是為了保持多態性


public class MyPlate implements Plate<T extends Comparable> {
    public MyPlate() {
    }

    public void set(Comparable t) {

    }

    public Comparable get() {
        return ....;
    }

    // 橋接函數,強制轉型
    @Override 
    public void set(Object t) {
        set((Compareable) t);
    }

    // 橋接函數,強制轉型
    @Override 
    public Comparable get(Object t) {
        return (Comparable)get();
    }
}

泛型擦除的殘留:反編譯

● 使用工具 jd-gui 打開,字節碼還是會殘留,所以打開時仍然是有泛型的 T,保留該泛型格式是對字節碼分析有好處的

● 而 保留訊息則是保留在 類的常量池中,這也就是為何明明被擦除成 Object 卻仍然可以反射的原因

// .java 檔案
class FruitPlate<T> implements Plate<T> {

    private LinkedList<T> list = new LinkedList<>();

    @Override
    public void put(T t) {
        list.add(t);
    }

    @Override
    public T get() {
        return list.peek();
    }

}

// javac 編譯過後的 class 檔案使用 jd-gui 開啟
package testMyJava.Generic;

import java.util.LinkedList;

class FruitPlate<T> implements Plate<T> {
  private LinkedList<T> list = new LinkedList<>();

  public void put(T paramT) {
    this.list.add(paramT);
  }

  public T get() {
    return this.list.peek();
  }
}


泛型使用注意

Heap Pollution 問題

Heap Pollution 是將一個 未定義參數型態的 Collection 物件指定給一個有定義參數型態的 Collection 物件


public class TestAnnotation {

    public static void main(String[] args) {
        List list = Arrays.asList(args);
        //"1. "
        ToDo.print(list);
    }

}

class ToDo {
    static void print (List<Integer> list) {
        for(int i : list) {
            System.out.println("value: " + i);
        }
    }
}

A. 將一個無泛型參數指向一個有泛型參數

這時編譯器會出現警告,因為傳入一個未定一類型的 Collection 類

雖然可編譯成功,但是很明顯傳入的類型應該是 String,執行時就會出現錯誤,ClassCastException

● 以下就會出現 ClassCastException,因為類型的不同,編譯檢查不支持泛型


class TestHeapPolluation {
    public void Test() {
        // 預設為 Object
        List list = new ArrayList<Integer>();
        list.add(3);

        List<String> list2 = list;
        String str = list2.get(0); // Err !
    }
}

更多的 Java 語言相關文章

Java 語言深入

● 在這個系列中,我們全方位地探討了 Java 語言的各個核心主題,旨在幫助你徹底掌握這門強大的編程語言。無論你是想深入理解 Java 的基礎類型與變數作用域,還是探索異常處理與運算子的細節,這些文章都將為您提供寶貴的知識

深入 Java 物件導向

● 探索 Java 物件導向的奧妙,掌握介面、抽象類、繼承等重要概念!幫助你針對物件導向設計有更深入的了解!


Leave a Comment

Comments

No comments yet. Why don’t you start the discussion?

發表迴響