--- title: 'Java 泛型' disqus: kyleAlien --- Java 泛型 === ## Overview of Content 主要有分為 **泛型類、泛型方法、泛型界面** :::success * 如果喜歡讀更好看一點的網頁版本,可以到我新做的網站 [**DevTech Ascendancy Hub**](https://devtechascendancy.com/) 本篇文章對應的是 [**深入了解 Java 泛型:從基本概念到實踐應用 | 3 種泛型 | Java、C++、C# 泛型概念**](https://devtechascendancy.com/java-generics-complete-guide/) ::: [TOC] ## Generic for what 對於相同的運算,或是流程不用重複寫相同的程式,以下程式可看到,明明都是加法的程式,不過就因為「類型(type)」不同就需要做多次相同程式的撰寫… 而使用泛型(Generic)就可以達到相同的目的,只撰寫一次程式(說繞口一點就是「**參數化類型**」) ```java= public int sumOfTwoInt(int a, int b) { return a + b; } public float sumOfTwoFloat(float a, float b) { return a + b; } ``` ### 泛型優點 * 如果有定義泛型就不用強制轉換,它會更加有效的使用「靜態程式的檢查特性」!讓產出的程式更加穩定,提早在「編譯期」就發現問題 > ![image](https://hackmd.io/_uploads/SypAQ69DR.png) * **對於不同類型(`Type`)的數據,也可以用相同的程式處理**,達到很好的兼容效果 **泛型如果未定義類型,是可以任意添加其他類型的值,Java 並不會檢查**,範例如下 ```java= // 未定義泛型時 List list = new ArrayList(); list.add("Hello"); list.add("World"); list.add(123); ``` > ![](https://i.imgur.com/ODmloWE.png) > > 將一個對象加入集合中,如果**未定義類型**就不會檢查其類型,取出時預設為 Object 類型 > 此時強制轉型會拋出異常,**ClassCastExecption** 強轉型錯誤 > ### 泛型的來源、原理 * JDK 5 後 Java 才加入泛型,往往有人稱 Java 的泛型為「偽泛型」,這其實是 為了向下兼容;另外在 JVM 虛擬機是不支持泛型的,所以 Java 語言為了實現泛型使用了 **==偽泛型==** :::success Java 會在 編譯期間擦除 所有的泛型訊息,這樣就可以讓 JVM 使用相同的字節碼而不必新增,在 JVM Runtime 時就不存在所謂的泛型訊息 (後面會介紹泛型擦除) ::: * 由於泛型擦除的存在,Java 的泛型是在編譯期間處理的,而不是在運行時… 這意味著在 JVM 運行時,不存在泛型類型的信息,所有的類型檢查和轉換都在編譯期間完成。這樣做的好處是可以簡化 JVM 的設計,並且確保向下兼容 :::info * **運行時類型轉換** 編譯器會在必要的地方插入類型轉換,以確保類型安全。這些轉換是在「運行時, Runtime」執行的,但它們是基於編譯期間插入的轉換操作 ::: ### Java 泛型副作用 * ++**不能使用基本數據類型**++ (byte、char、short、int、long、double),因為編譯過後會經過擦除,擦除後後會轉換為 Object 類型,在轉換回特定類型時無法得知「目標類的類資訊」 * **++不能++使用 `instanceof` 運算符號**,同樣是因為類型擦除後所帶來的副作用,會導致無法判別類型(`Type`) > 擦拭後都是 Object 類型,沒得判斷 > > ![image](https://hackmd.io/_uploads/H1sNPaqw0.png) * 「泛型類」不能使用在靜態:因為泛型創建物件時才能確定,但靜態方法、靜態參數,是不用加載就可以使用 (泛型方法則可以,因為泛型方法是呼叫後才加載) > ![image](https://hackmd.io/_uploads/By-IY6cPC.png) * **泛型可能會導致多載方法衝突**,同樣是因為擦除後會轉為 Object 類,導致++重複定義++ ```java= // 泛型 public void HelloWorld(T t) { // 編譯字節碼後,同 Object t ... } public void HelloWorld(Object t) { ... } ``` * **無法創建泛型對象,但==可以使用反射創建泛型對象 (因為反射就是 Runtime 運行)==** ```java= 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(); } ``` :::warning * **Java 中沒有所謂的泛型數組**,同樣也是因為擦除,無法判斷是否是同一類型 ```java= // 非法:不能創建泛型數組 List<String>[] stringLists = new List<String>[10]; ``` > ![image](https://hackmd.io/_uploads/rJIBqT9wA.png) ::: ## 泛型類、界面、方法 * **泛型的本質是為了==參數化類型==**,也就是指定參數類型,把類型當成引數一樣傳遞 * 在泛型的使用中,操作數據類型被指定為一參數,這參數可被用在 `Class`、`interface` and `method` * **泛型允許多個變量(多個參數類型)**<T, K, V>,引入一個類型變數 T (其他字母也可以 T, E, V, K 等等) 並用 `<>` 包住 ### Generic Class:泛型類 * 泛型類是以類(`Class`)為基礎再加上泛型類型的符號就可以使用,之後這個類就可以使用泛型指定的類型 ```java= public class Clz<T> { private T data; public Clz(T data) { this.data = data; } } ``` 以下範例 (類型變數用 T 代表),T 為數據類型 ```java= 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; } } ``` **---實作---** > ![](https://i.imgur.com/6GZs1Wf.png) ### Generic Interface:泛型界面 * 泛型界面基本上跟 Class 相同,不過泛型界面是使用界面(`interface`)作為基礎,再加上泛型類型的符號,之後就可以使用泛型界面 ```java= public interface GInterface<T> { <T> getData(); } ``` 以下範例使用,兩種實現方式來使用泛型界面,分別是 ^1.^ 實作界面時未指定類型(將指定類型的責任往使用該類者轉移),^2.^ 實作界面後指定類型(泛型類同樣也可以這樣使用) ```java= // 泛型界面 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(); } ``` **---實作---** > ![](https://i.imgur.com/me4DXjA.png) * **泛型類(`Generic Class`)的繼承規則**:泛型類可以繼承或是拓展其他泛型類;像是以下範例,`NormalClass` 實作泛型類 `MyClz` ```java= 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 Method:泛型方法 * Generic Method 可使用在任何場景的類中… 包括泛型類、普通類(也就是不單指定泛型類) ```java= // 泛型類型 class GenericClz<T> { // 泛型方法 void setData(T data) { // TODO: } } // 普通類 class NormalClz { // 泛型方法 <T> void setData() { } } ``` * 「**混和型**」也可以使用 Generic Method,可載入不同於「類」定義時的類型… 範例如下: ```java= 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 and 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]; } } ``` **---實作---** > ![](https://i.imgur.com/3tIDKcC.png) ## 限定類型 Qulified Type 有時會對類型變量進型約束,例如約束型態 T 必須要實現 Comparable 界面 `<T extends Comparable> ` > 如果不進行約束將無法確保該型態是否可執行此界面的函數 ```java= // 未用現定符號,「無法」確定是否都有實現 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>` :::danger **注意** 限定類型中 ==只能有一類 (一個繼承),而且必須是第一個== ! ::: * 可限定多個類型,使用 `&` 連接多個限制條件 `<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; } ``` **---實作---** > ![](https://i.imgur.com/TKP9JxC.png) * 想看更多的範例,可參考 Java 源碼中實現的 `List` and `ArrayList` 類,這些類型都有使用泛型約束... > ![](https://i.imgur.com/W1EPGdw.png) ## Universal Symbol 通配符 Java 的泛型有另外一個概念「通配符號, Universal Symbol」,通配符會使用「問號 `?`」來呈現 **它有兩種拓展使用方式:`extends`、`super` ,它們個代表了不同的限制,而在了解通配符之前要先了解,為何要使用它 ? 這就要先了解 繼承 & 泛型的關係** ### 泛型類繼承 * **泛型類繼承** 1. 泛型雖然在編譯過後在字節碼(`Byte Code`)被擦除,但是仍會在檢查的時候用到,只要 **泛型類型的原型基類相同,就符合泛型類繼承** 2. 如果與基類不同就無法實現泛型類繼承… 簡單範例如下 ```java= 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> c3 = 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> { } ``` **--實作--** > ![image](https://hackmd.io/_uploads/BylEAJ_dC.png) ### 通配符功能:讓泛型類之間產生關係 * 要了解通配符能為泛型帶來什麼樣的功能,先要了解 **泛型類之間的關係**,在以下範例中我們來觀察一下,泛型是否可以判斷「繼承」的特性? ```java= 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 { } ``` 1. 在一般類時可賦予值,所以 ok(但是不夠好,無法善用靜態語言的檢查特性) 2. 無法使用的原因是它規定了傳入的參數必須是 `Shop<Fruit>`,**雖然一般的繼承有關係,但是++到了==泛型類別時它無法自動判斷是否有關係==++**,它們全都繼承於 Object 類,導致編譯器無法判斷兩個類之間的關係 (因為 ++Shop 才是主體,泛型會被擦除++,所以繼承於 Object) > ![](https://i.imgur.com/oECAnVC.png) 3. 同樣的,也 **無法強制轉型**,Java 不允許這樣的操作,會提醒我們 Inconvertible types; cannot cast 錯誤 :::info * 從這個例子中,可以引出 **要 ==讓普通繼承 & 泛型++產生關係++就要使用通配符==** ::: ### 使用通配符 * 若泛型使用了 通配符 `?`(單單只有通配符),那就不能設定也不能取值,**==目的是為了 ++類型檢查++==**… 範例如下 ```java= 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 { } ``` **--實作--** > ![](https://i.imgur.com/zm0Ul9d.png) * 若泛型使用了 通配符 `?`(單單只有通配符),但是作為「引數」,代表 全部類型都可以接受 ### 通配符 & extends * **通配符與 `extends` 配合後的功能是:==安全的 ++取得 (get)++ 數據==**;**作為引數**,基礎使用 `<? extends X>`,表示類型的 **++上限到 X 類++,包含 X 類以及其衍生子類** ```java= 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 !! } ``` 1. 使用 `<? extends Fruit>` 拓展了傳入泛型類別的寬度,拓展範圍是 Fruit 跟它的子類 (include Fruit) 2. 可**安全的取得類,因為返回的一定是 Fruit 類** (上限到 Fruit) 3. **返回的是 Fruit 類,它的 ++子類要強制轉型++** 4. 只知道設定的是 apple,但卻**不能具體的知道 Shop 內的類型變數 T 要設定哪個類** (可能是 Fruit、Apple、HonFuShi、Orang) :::success **主要用來安全的訪問(取得)數據**,就像是 Kotlin 的協變 (out) ::: > ![](https://i.imgur.com/NbHzyYS.png) * 範例二:**強調通配符 extends 是限定泛型**,extends 可安全的取出 ```java= 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 { } ``` **--實作--** > ![](https://i.imgur.com/kVajozS.png) ### 通配符 & super * **通配符與 `super` 配合的功能是:==安全的 ++設定++ 數據==**;**作為引數**,基礎使用 `<? super X>`,表示類型的 **++下限是 X++ 類,包含 X 類 以及其父類(超類)** :::warning **限定符不能使用 super**,像是如果使用 **`<T super Shop>` 語法錯誤** ::: ```java= 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()); } ``` 1. 使用 `<? super Fruit>` 拓展了傳入泛型類別的寬度,拓展範圍是 `Fruit` 跟它的父類(include Fruit),所以不符合入參拓展規定 2. 編譯器不知道set 它的具體類型,但可**保證 `Fruit` and 其 `子類` 可安全轉型成 Fruit,set 無法得知設定的超類是否能轉型成 Fruit** 3. **返回的類別是 Object,無法得知回傳的具體類別**(有可能是 Food、Fruit),**但一定是 Object 的子類** 4. 返回 Object 類別,除非強轉否則無法使用指定的子類 Function :::success **主要用來安全的設定 (set) 數據**,只可寫入 X 的子類,就像是 Kotlin 的逆變 (in) ::: > ![](https://i.imgur.com/uxsX8fo.png) * 範例二:集合的安全設定 (set) 就可以使用到 `通配符 + super` ```java= 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 { } ``` :::info * 為何安全設定數據,但無法安全取得數據 ? **Get 時無法確定是哪一個類,主要原因是因為泛型限制丟失**,只能用 Object 存放 ::: > ![](https://i.imgur.com/TUq9QHB.png) ### super & extends 關係 & 使用 * `Shop<?>` 非限定通配符,等同於 Shop<\? extends Object> * `Shop<? extends T>`(上界)、`Shop<? super T>`(下屆) 兩者為 **限定通配符** ```java= 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 { } ``` 配合上面的程式看下面限定 & 非限定通配符的關係圖 > ![](https://i.imgur.com/oNJH5it.png) ### Java Collections 通配符使用 * Java Collections 內大量地使用了泛型 `super` & `extends`,以 copy 為例 **使用了 ++super 安全的設定 T 的衍生類數據++**,**使用 ++extends 安全的取得了 T 的衍生類數據++**,以此做為 copy 基礎 ```java= 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`,可以看下面的例子 1. 類關係 ```java= class PC { int id; PC(int id){ this.id = id; } } class CPU extends PC { int id; CPU(int id){ super(id); this.id = id; } } ``` 2. Collections 的 copy 函數使用 ```java= // 指定 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); } ``` 3. 測試 ```java= 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` 更加完善 > ![](https://i.imgur.com/o49uye5.png) ### 通配符 vs. 限定類型泛型 * 限定類型的泛型 **`T` 必須是具體的某一種類**,會有類型之間的轉換問題 * **通配符 `?` 讓泛型轉型更靈活**,**==反射可以打破通配符的限制==** > **類似於限定類別 `<T extends X>`,但限定類別用於限定泛型的類型** > > **通配符 `<? extends X>` 用於,限定/拓展 Func 接收的參數,並給予它限制** | 限定符類型 | 範例 | | -------- | -------- | | 非限定通配符 | `Plate<?>` or `Plate<? extends Object>` | | 限定通配符(指定類型) | `Plate<? extends Fruit>` or `Plate<? super Fruit>` | ## 虛擬機是如何實現泛型 最早是出現在 C++ 的模板(Temple),Java 只能通過 Object 是所以類的父類 and 強轉兩個特點型成泛型,Object 可轉型成任何類,只有在運行期才會知道是什麼類,編譯期無法檢察 Object 是否轉型成功,因此有許多的 `ClassCastException` 風險會轉駕到執行期 ### C++ 模板 * C++ 內部有真正的泛型 > 最早是出現在 C++ 的 [**模板(Temple)**](https://hackmd.io/KIxpdZ8lRKisv8N3UhvklQ#C-%E6%A8%A1%E6%9D%BF) ```cpp= 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](https://zh.wikipedia.org/wiki/%E9%80%9A%E7%94%A8%E8%AA%9E%E8%A8%80%E9%81%8B%E8%A1%8C%E5%BA%AB)) **List<int\>、 List<String\> 是兩種不同類型,都有自己的虛擬方法表**,這種實現稱為`類型膨脹`,基於這種方法型成的泛型為 **真實泛型** ### Java - 偽泛型 * **Java / kotlin 虛擬機其實內部並無泛型**,並不是真正的泛型 ,JDK 5 才有泛型,==為了向上兼容,才會演變成 ++偽泛型++== > 在編譯後的字節碼中以替換成原生類型(Raw Type),並在相應的第方插入強制轉型代碼,所以對於運行期的 **Java 語言來說 `List<Integer>`、`List<String>` 就是同一類別** > > Java 語言中的泛型實現方法稱為 ==**類型擦除**==,基於這種方法實現的泛型是**偽泛型** * **在泛型中並不是所有的擦除都會變成 Object 的,當泛型有限制性,Test<T extends Apple\>,則會被擦除成 Apple** ```java= List list = new ArrayList<Integer>(); List<String> list2 = list; ``` > 擦除過後 ArrayList<Integer\> 轉為 Array List,List<String\> 轉為 List ## Java 泛型擦除 * 在編譯成字節碼後,**泛型會擦除**,如果**沒有限定會變成 Object,如果有限定則會擦除成限定類** ```java= /** * 源碼 `.java` 檔 */ public interface Plate<T> { void set(T t); T get(); } /** * 擦除後 `.class` 檔 */ public interface Plate { void set(Object t); // 轉換為 Object Object get(); } ``` * 在擦除完後,當有需要時就會新增一個 **「橋接函數」,調用時用橋接函數,並且++強制轉型++再呼叫轉型的方法** > 生成橋方法的目的也是為了保持多態性 ```java= 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= // .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(); } } ``` **--實作--** > ![](https://i.imgur.com/Yy6Rce1.png) ## 泛型使用注意 ### Heap Pollution * `Heap Pollution` 是將一個 **未定義參數型態的 Collection 物件指定給一個有定義參數型態的 Collection 物件** ```java= 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); } } } ``` 1. 將一個==無泛型參數指向一個有泛型參數== > 這時編譯器會出現警告,因為傳入一個未定一類型的 Collection 類 > > 雖然可編譯成功,但是很明顯傳入的類型應該是 String,執行時就會出現錯誤,**ClassCastException** * 以下就會出現 ClassCastException,因為類型的不同,**編譯檢查不支持泛型** ```java= class TestHeapPolluation { public void Test() { // 預設為 Object List list = new ArrayList<Integer>(); list.add(3); List<String> list2 = list; String str = list2.get(0); // Err ! } } ``` ## 重點回顧 * **Array 中不可以使用泛型** > 實現就不支持 * ArrayList<String\> a = new ArrayList<Object\> ? > 雖然 String & Object 是繼承關係,但是 **泛型中是沒有關係的** * Set & Set<\?> 區別 ? > Set 不受檢查機制檢查,Set<\?> 仍要接受類型檢查 * List<\?> & List<Object\> 的差別? > 使用通配符號,傳入的引數沒有限制,Object 有限制 * 泛型是啥? 泛型好處? > **參數化類型機制** > 1. 代碼復用 > 2. 類型檢查推到編譯期間 * 泛型如何工作的? 類型擦除 > Java 中是偽泛型,編譯期間會去檢查泛型類型 > Java 虛擬機不支持泛型,所以在轉變為 二進制時就會擦除類型 * 限定通配符號 > <\? extends String> * PECS > PE : Product Extends <\T extends Object> > CS : Consumer Super <\T super Object> * 無法運行時檢查 > if(a instances Object) // Err ## Appendix & FAQ :::info <!-- Where is different between the universal symbol and Qulified Type? --> ::: ###### tags: `Java 基礎進階` `Java 泛型`