- 資訊首頁(yè) > 開(kāi)發(fā)技術(shù) >
- 一篇文章弄懂Java和Kotlin的泛型難點(diǎn)
Java 和 Kotlin 的泛型算作是一塊挺大的知識難點(diǎn)了,涉及到很多很難理解的概念:泛型型參、泛型實(shí)參、類(lèi)型參數、不變、型變、協(xié)變、逆變、內聯(lián)等等。本篇文章就將 Java 和 Kotlin 結合著(zhù)一起講,按照我的個(gè)人理解來(lái)闡述泛型的各個(gè)知識難點(diǎn),希望對你有所幫助 😇😇
泛型允許你定義帶類(lèi)型形參的數據類(lèi)型,當這種類(lèi)型的實(shí)例被創(chuàng )建出來(lái)后,類(lèi)型形參便被替換為稱(chēng)為類(lèi)型實(shí)參的具體類(lèi)型。例如,對于 List<T>,List 稱(chēng)為基礎類(lèi)型,T 便是類(lèi)型型參,T 可以是任意類(lèi)型,當沒(méi)有指定 T 的具體類(lèi)型時(shí),我們只能知道List<T>是一個(gè)集合列表,但不知道承載的具體數據類(lèi)型。而對于 List<String>,當中的 String 便是類(lèi)型實(shí)參,我們可以明白地知道該列表承載的都是字符串,在這里 String 就相當于一個(gè)參數傳遞給了 List,在這語(yǔ)義下 String 也稱(chēng)為類(lèi)型參數
此外,在 Kotlin 中我們可以實(shí)現實(shí)化類(lèi)型參數,在運行時(shí)的內聯(lián)函數中拿到作為類(lèi)型實(shí)參的具體類(lèi)型,即可以實(shí)現 T::class.java,但在 Java 中卻無(wú)法實(shí)現,因為內聯(lián)函數是 Kotlin 中的概念,Java 中并不存在
泛型是在 Java 5 版本開(kāi)始引入的,先通過(guò)幾個(gè)小例子來(lái)明白泛型的重要性
以下代碼可以成功編譯,但是在運行時(shí)卻拋出了 ClassCastException。了解 ArrayList 源碼的同學(xué)就知道其內部是用一個(gè)Object[]數組來(lái)存儲數據的,這使得 ArrayList 能夠存儲任何類(lèi)型的對象,所以在沒(méi)有泛型的年代開(kāi)發(fā)者一不小心就有可能向 ArrayList 存入了非期望值,編譯期完全正常,等到在運行時(shí)就會(huì )拋出類(lèi)型轉換異常了
public class GenericTest { public static void main(String[] args) { List stringList = new ArrayList(); addData(stringList); String str = (String) stringList.get(0); } public static void addData(List dataList) { dataList.add(1); } }
Exception in thread "main" java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
而有了泛型后,我們就可以寫(xiě)出更加健壯安全的代碼,以下錯誤就完全可以在編譯階段被發(fā)現,且取值的時(shí)候也不需要進(jìn)行類(lèi)型強轉
public static void main(String[] args) { List<String> stringList = new ArrayList(); addData(stringList); //報錯 String str = stringList.get(0); } public static void addData(List<Integer> dataList) { dataList.add(1); }
此外,利用泛型我們可以寫(xiě)出更加具備通用性的代碼。例如,假設我們需要從一個(gè) List 中篩選出大于 0 的全部數字,那我們自然不想為 Integer、Float、Double 等多種類(lèi)型各寫(xiě)一個(gè)篩選方法,此時(shí)就可以利用泛型來(lái)抽象篩選邏輯
public static void main(String[] args) { List<Integer> integerList = new ArrayList<>(); integerList.add(-1); integerList.add(1); integerList.add(2); List<Integer> result1 = filter(integerList); List<Float> floatList = new ArrayList<>(); floatList.add(-1f); floatList.add(1f); floatList.add(2f); List<Float> result2 = filter(floatList); } public static <T extends Number> List<T> filter(List<T> data) { List<T> filterList = new ArrayList<>(); for (T datum : data) { if (datum.doubleValue() > 0) { filterList.add(datum); } } return filterList; }
總的來(lái)說(shuō),泛型有以下幾點(diǎn)優(yōu)勢:
泛型是在 Java 5 版本開(kāi)始引入的,所以在 Java 4 中 ArrayList 還不屬于泛型類(lèi),其內部通過(guò) Object 向上轉型和外部強制類(lèi)型轉換來(lái)實(shí)現數據存儲和邏輯復用,此時(shí)開(kāi)發(fā)者的項目中已經(jīng)充斥了大量以下類(lèi)型的代碼:
List stringList = new ArrayList(); stringList.add("業(yè)志陳"); stringList.add("https://juejin.cn/user/923245496518439"); String str = (String) stringList.get(0);
而在推出泛型的同時(shí),Java 官方也必須保證二進(jìn)制的向后兼容性,用 Java 4 編譯出的 Class 文件也必須能夠在 Java 5 上正常運行,即 Java 5 必須保證以下兩種類(lèi)型的代碼能夠在 Java 5 上共存且正常運行
List stringList = new ArrayList(); List<String> stringList = new ArrayList();
為了實(shí)現這一目的,Java 就通過(guò)類(lèi)型擦除這種比較別扭的方式來(lái)實(shí)現泛型。編譯器在編譯時(shí)會(huì )擦除類(lèi)型實(shí)參,在運行時(shí)不存在任何類(lèi)型相關(guān)的信息,泛型對于 JVM 來(lái)說(shuō)是透明的,有泛型和沒(méi)有泛型的代碼通過(guò)編譯器編譯后所生成的二進(jìn)制代碼是完全相同的
例如,分別聲明兩個(gè)泛型類(lèi)和非泛型類(lèi),拿到其 class 文件
public class GenericTest { public static class NodeA { private Object obj; public NodeA(Object obj) { this.obj = obj; } } public static class NodeB<T> { private T obj; public NodeB(T obj) { this.obj = obj; } } public static void main(String[] args) { NodeA nodeA = new NodeA("業(yè)志陳"); NodeB<String> nodeB = new NodeB<>("業(yè)志陳"); System.out.println(nodeB.obj); } }
可以看到 NodeA 和 NodeB 兩個(gè)對象對應的字節碼其實(shí)是完全一樣的,最終都是使用 Object 來(lái)承載數據,就好像傳遞給 NodeB 的類(lèi)型參數 String 不見(jiàn)了一樣,這便是類(lèi)型擦除
public class generic.GenericTest { public generic.GenericTest(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return public static void main(java.lang.String[]); Code: 0: new #2 // class generic/GenericTest$NodeA 3: dup 4: ldc #3 // String 業(yè)志陳 6: invokespecial #4 // Method generic/GenericTest$NodeA."<init>":(Ljava/lang/Object;)V 9: astore_1 10: new #5 // class generic/GenericTest$NodeB 13: dup 14: ldc #3 // String 業(yè)志陳 16: invokespecial #6 // Method generic/GenericTest$NodeB."<init>":(Ljava/lang/Object;)V 19: astore_2 20: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream; 23: aload_2 24: invokestatic #8 // Method generic/GenericTest$NodeB.access$000:(Lgeneric/GenericTest$NodeB;)Ljava/lang/Object; 27: checkcast #9 // class java/lang/String 30: invokevirtual #10 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 33: return }
而如果讓 NodeA 直接使用 String 類(lèi)型,并且為泛型類(lèi) NodeB 設定上界約束 String,兩者的字節碼也會(huì )完全一樣
public class GenericTest { public static class NodeA { private String obj; public NodeA(String obj) { this.obj = obj; } } public static class NodeB<T extends String> { private T obj; public NodeB(T obj) { this.obj = obj; } } public static void main(String[] args) { NodeA nodeA = new NodeA("業(yè)志陳"); NodeB<String> nodeB = new NodeB<>("業(yè)志陳"); System.out.println(nodeB.obj); } }
可以看到 NodeA 和 NodeB 的字節碼是完全相同的
public class generic.GenericTest { public generic.GenericTest(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return public static void main(java.lang.String[]); Code: 0: new #2 // class generic/GenericTest$NodeA 3: dup 4: ldc #3 // String 業(yè)志陳 6: invokespecial #4 // Method generic/GenericTest$NodeA."<init>":(Ljava/lang/String;)V 9: astore_1 10: new #5 // class generic/GenericTest$NodeB 13: dup 14: ldc #3 // String 業(yè)志陳 16: invokespecial #6 // Method generic/GenericTest$NodeB."<init>":(Ljava/lang/String;)V 19: astore_2 20: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream; 23: aload_2 24: invokestatic #8 // Method generic/GenericTest$NodeB.access$000:(Lgeneric/GenericTest$NodeB;)Ljava/lang/String; 27: invokevirtual #9 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 30: return }
所以說(shuō),當泛型類(lèi)型被擦除后有兩種轉換方式
該結論也可以通過(guò)反射泛型類(lèi)的 Class 對象來(lái)驗證
public class GenericTest { public static class NodeA<T> { private T obj; public NodeA(T obj) { this.obj = obj; } } public static class NodeB<T extends String> { private T obj; public NodeB(T obj) { this.obj = obj; } } public static void main(String[] args) { NodeA<String> nodeA = new NodeA<>("業(yè)志陳"); getField(nodeA.getClass()); NodeB<String> nodeB = new NodeB<>("https://juejin.cn/user/923245496518439"); getField(nodeB.getClass()); } private static void getField(Class clazz) { for (Field field : clazz.getDeclaredFields()) { System.out.println("fieldName: " + field.getName()); System.out.println("fieldTypeName: " + field.getType().getName()); } } }
NodeA 對應的是 Object,NodeB 對應的是 String
fieldName: obj fieldTypeName: java.lang.Object fieldName: obj fieldTypeName: java.lang.String
那既然在運行時(shí)不存在任何類(lèi)型相關(guān)的信息,泛型又為什么能夠實(shí)現類(lèi)型檢查和類(lèi)型自動(dòng)轉換等功能呢?
其實(shí),類(lèi)型檢查是編譯器在編譯前幫我們完成的,編譯器知道我們聲明的具體的類(lèi)型實(shí)參,所以類(lèi)型擦除并不影響類(lèi)型檢查功能。而類(lèi)型自動(dòng)轉換其實(shí)是通過(guò)內部強制類(lèi)型轉換來(lái)實(shí)現的,上面給出的字節碼中也可以看到有一條類(lèi)型強轉 checkcast 的語(yǔ)句
27: checkcast #9 // class java/lang/String
例如,ArrayList 內部雖然用于存儲數據的是 Object 數組,但 get 方法內部會(huì )自動(dòng)完成類(lèi)型強轉
transient Object[] elementData; public E get(int index) { rangeCheck(index); return elementData(index); } @SuppressWarnings("unchecked") E elementData(int index) { //強制類(lèi)型轉換 return (E) elementData[index]; }
所以 Java 的泛型可以看做是一種特殊的語(yǔ)法糖,因此也被人稱(chēng)為偽泛型
Java 泛型對于類(lèi)型的約束只在編譯期存在,運行時(shí)仍然會(huì )按照 Java 5 之前的機制來(lái)運行,泛型的具體類(lèi)型在運行時(shí)已經(jīng)被刪除了,所以 JVM 是識別不到我們在代碼中指定的具體的泛型類(lèi)型的
例如,雖然List<String>只能用于添加字符串,但我們只能泛化地識別到它屬于List<?>類(lèi)型,而無(wú)法具體判斷出該 List 內部包含的具體類(lèi)型
List<String> stringList = new ArrayList<>(); //正常 if (stringList instanceof ArrayList<?>) { } //報錯 if (stringList instanceof ArrayList<String>) { }
我們只能對具體的對象實(shí)例進(jìn)行類(lèi)型校驗,但無(wú)法判斷出泛型形參的具體類(lèi)型
public <T> void filter(T data) { //正常 if (data instanceof String) { } //報錯 if (T instanceof String) { } //報錯 Class<T> tClass = T::getClass; }
此外,類(lèi)型擦除也會(huì )導致 Java 中出現多態(tài)問(wèn)題。例如,以下兩個(gè)方法的方法簽名并不完全相同,但由于類(lèi)型擦除的原因,入參參數的數據類(lèi)型都會(huì )被看成 List<Object>,從而導致兩者無(wú)法共存在同一個(gè)區域內
public void filter(List<String> stringList) { } public void filter(List<Integer> stringList) { }
Kotlin 泛型在大體上和 Java 一致,畢竟兩者需要保證兼容性
class Plate<T>(val t: T) { fun cut() { println(t.toString()) } } class Apple class Banana fun main() { val plateApple = Plate<Apple>(Apple()) //泛型類(lèi)型自動(dòng)推導 val plateBanana = Plate(Banana()) plateApple.cut() plateBanana.cut() }
Kotlin 也支持在擴展函數中使用泛型
fun <T> List<T>.find(t: T): T? { val index = indexOf(t) return if (index > -1) get(index) else null }
需要注意的是,為了實(shí)現向后兼容,目前高版本 Java 依然允許實(shí)例化沒(méi)有具體類(lèi)型參數的泛型類(lèi),這可以說(shuō)是一個(gè)對新版本 JDK 危險但對舊版本友好的兼容措施。但 Kotlin 要求在使用泛型時(shí)需要顯式聲明泛型類(lèi)型或者是編譯器能夠類(lèi)型推導出具體類(lèi)型,任何不具備具體泛型類(lèi)型的泛型類(lèi)都無(wú)法被實(shí)例化。因為 Kotlin 一開(kāi)始就是基于 Java 6 版本的,一開(kāi)始就存在了泛型,自然就不存在需要兼容老代碼的問(wèn)題,因此以下例子和 Java 會(huì )有不同的表現
val arrayList1 = ArrayList() //錯誤,編譯器報錯 val arrayList2 = arrayListOf<Int>() //正常 val arrayList3 = arrayListOf(1, 2, 3) //正常
還有一個(gè)比較容易讓人誤解的點(diǎn)。我們經(jīng)常會(huì )使用 as 和 as? 來(lái)進(jìn)行類(lèi)型轉換,但如果轉換對象是泛型類(lèi)型的話(huà),那就會(huì )由于類(lèi)型擦除而出現誤判。如果轉換對象有正確的基礎類(lèi)型,那么轉換就會(huì )成功,而不管類(lèi)型實(shí)參是否相符。因為在運行時(shí)轉換發(fā)生的時(shí)候類(lèi)型實(shí)參是未知的,此時(shí)編譯器只會(huì )發(fā)出 “unchecked cast” 警告,代碼還是可以正常編譯的
例如,在以下例子中代碼的運行結果還符合我們的預知。第一個(gè)轉換操作由于類(lèi)型相符,所以打印出了相加值。第二個(gè)轉換操作由于基礎類(lèi)型是 Set 而非 List,所以?huà)伋隽?IllegalAccessException
fun main() { printSum(listOf(1, 2, 3)) //6 printSum(setOf(1, 2, 3)) //IllegalAccessException } fun printSum(c: Collection<*>) { val intList = c as? List<Int> ?: throw IllegalAccessException("List is expected") println(intList.sum()) }
而在以下例子中拋出的卻是 ClassCastException,這是因為在運行時(shí)不會(huì )判斷且無(wú)法判斷出類(lèi)型實(shí)參到底是否是 Int,而只會(huì )判斷基礎類(lèi)型 List 是否相符,所以 as? 操作會(huì )成功,等到要執行相加操作時(shí)才會(huì )發(fā)現拿到的是 String 而非 Number
printSum(listOf("1", "2", "3")) Exception in thread "main" java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Number
泛型本身已經(jīng)帶有類(lèi)型約束的作用,我們也可以進(jìn)一步細化其支持的具體類(lèi)型
例如,假設存在一個(gè)盤(pán)子 Plate,我們要求該 Plate 只能用于裝水果 Fruit,那么就可以對其泛型聲明做進(jìn)一步約束,Java 中使用 extend 關(guān)鍵字來(lái)聲明約束規則,而 Kotlin 使用的是 : 。這樣 Plate 就只能用于 Fruit 和其子類(lèi),而無(wú)法用于 Noodles 等不相關(guān)的類(lèi)型,這種類(lèi)型約束就被稱(chēng)為上界約束
open class Fruit class Apple : Fruit() class Noodles class Plate<T : Fruit>(val t: T) fun main() { val applePlate = Plate(Apple()) //正常 val noodlesPlate = Plate(Noodles()) //報錯 }
如果上界約束擁有多層類(lèi)型元素,Java 是使用 & 符號進(jìn)行鏈式聲明,Kotlin 則是用 where 關(guān)鍵字來(lái)依次進(jìn)行聲明
interface Soft class Plate<T>(val t: T) where T : Fruit, T : Soft open class Fruit class Apple : Fruit() class Banana : Fruit(), Soft fun main() { val applePlate = Plate(Apple()) //報錯 val bananaPlate = Plate(Banana()) //正常 }
此外,沒(méi)有指定上界約束的類(lèi)型形參會(huì )默認使用 Any? 作為上界,即我們可以使用 String 或 String? 作為具體的類(lèi)型實(shí)參。如果想確保最終的類(lèi)型實(shí)參一定是非空類(lèi)型,那么就需要主動(dòng)聲明上界約束為 Any
假設現在有個(gè)需求,需要我們提供一個(gè)方法用于遍歷所有類(lèi)型的 List 集合并打印元素
第一種做法就是直接將方法參數類(lèi)型聲明為 List,不包含任何泛型類(lèi)型聲明。這種做法可行,但編譯器會(huì )警告無(wú)法確定 list元素的具體類(lèi)型,所以這不是最優(yōu)解法
public static void printList1(List list) { for (Object o : list) { System.out.println(o); } }
可能會(huì )想到的第二種做法是:將泛型類(lèi)型直接聲明為 Object,希望讓其適用于任何類(lèi)型的 List。這種做法完全不可行,因為即使 String 是 Object 的子類(lèi),但 List<String> 和 List<Object>并不具備從屬關(guān)系,這導致 printList2 方法實(shí)際上只能用于List<Object>這一種具體類(lèi)型
public static void printList2(List<Object> list) { for (Object o : list) { System.out.println(o); } }
最優(yōu)解法就是要用到 Java 的類(lèi)型通配符 ? 了,printList3方法完全可行且編譯器也不會(huì )警告報錯
public static void printList3(List<?> list) { for (Object o : list) { System.out.println(o); } }
? 表示我們并不關(guān)心具體的泛型類(lèi)型,而只是想配合其它類(lèi)型進(jìn)行一些條件限制。例如,printList3方法希望傳入的是一個(gè) List,但不限制泛型的具體類(lèi)型,此時(shí)List<?>就達到了這一層限制條件
類(lèi)型通配符也存在著(zhù)一些限制。因為 printList3 方法并不包含具體的泛型類(lèi)型,所以我們從中取出的值只能是 Object 類(lèi)型,且無(wú)法向其插入值,這都是為了避免發(fā)生 ClassCastException
Java 的類(lèi)型通配符對應 Kotlin 中的概念就是**星號投影 * **,Java 存在的限制在 Kotlin 中一樣有
fun printList(list: List<*>) { for (any in list) { println(any) } }
此外,星號投影只能出現在類(lèi)型形參的位置,不能作為類(lèi)型實(shí)參
val list: MutableList<*> = ArrayList<Number>() //正常 val list2: MutableList<*> = ArrayList<*>() //報錯
看以下例子。Apple 和 Banana 都是 Fruit 的子類(lèi),可以發(fā)現 Apple[] 類(lèi)型的對象是可以賦值給 Fruit[] 的,且 Fruit[] 可以容納 Apple 對象和 Banana 對象,這種設計就被稱(chēng)為協(xié)變,即如果 A 是 B 的子類(lèi),那么 A[] 就是 B[] 的子類(lèi)型。相對的,Object[] 就是所有數組對象的父類(lèi)型
static class Fruit { } static class Apple extends Fruit { } static class Banana extends Fruit { } public static void main(String[] args) { Fruit[] fruitArray = new Apple[10]; //正常 fruitArray[0] = new Apple(); //編譯時(shí)正常,運行時(shí)拋出 ArrayStoreException fruitArray[1] = new Banana(); }
而 Java 中的泛型是不變的,這意味著(zhù) String 雖然是 Object 的子類(lèi),但List<String>并不是List<Object>的子類(lèi)型,兩者并不具備繼承關(guān)系
List<String> stringList = new ArrayList<>(); List<Object> objectList = stringList; //報錯
那為什么 Java 中的泛型是不變的呢?
這可以通過(guò)看一個(gè)例子來(lái)解釋。假設 Java 中的泛型是協(xié)變的,那么以下代碼就可以成功通過(guò)編譯階段的檢查,在運行時(shí)就不可避免地將拋出 ClassCastException,而引入泛型的初衷就是為了實(shí)現類(lèi)型安全,支持協(xié)變的話(huà)那泛型也就沒(méi)有比數組安全多少了,因此就將泛型被設計為不變的
List<String> strList = new ArrayList<>(); List<Object> objs = strList; //假設可以運行,實(shí)際上編譯器會(huì )報錯 objs.add(1); String str = strList.get(0); //將拋出 ClassCastException,無(wú)法將整數轉換為字符串
再來(lái)想個(gè)問(wèn)題,既然協(xié)變本身并不安全,那么數組為何又要被設計為協(xié)變呢?
Arrays 類(lèi)包含一個(gè) equals方法用于比較兩個(gè)數組對象是否相等。如果數組是協(xié)變的,那么就需要為每一種數組對象都定義一個(gè) equals方法,包括開(kāi)發(fā)者自定義的數據類(lèi)型。想要避免這種情況,就需要讓 Object[] 可以接收任意數組類(lèi)型,即讓 Object[] 成為所有數組對象的父類(lèi)型,這就使得數組必須支持協(xié)變,這樣多態(tài)才能生效
public class Arrays { public static boolean equals(Object[] a, Object[] a2) { if (a==a2) return true; if (a==null || a2==null) return false; int length = a.length; if (a2.length != length) return false; for (int i=0; i<length; i++) { Object o1 = a[i]; Object o2 = a2[i]; if (!(o1==null ? o2==null : o1.equals(o2))) return false; } return true; } }
需要注意的是,Kotlin 中的數組和 Java 中的數組并不一樣,Kotlin 數組并不支持協(xié)變,Kotlin 數組類(lèi)似于集合框架,具有對應的實(shí)現類(lèi) Array,Array 屬于泛型類(lèi),支持了泛型因此也不再協(xié)變
val stringArray = arrayOfNulls<String>(3) val anyArray: Array<Any?> = stringArray //報錯
Java 的泛型也并非完全不變的,只是實(shí)現協(xié)變需要滿(mǎn)足一些條件,甚至也可以實(shí)現逆變,下面就來(lái)介紹下泛型如何實(shí)現協(xié)變和逆變
假設我們定義了一個(gè)copyAll希望用于 List 數據遷移。那以下操作在我們看來(lái)就是完全安全的,因為 Integer 是 Number 的子類(lèi),按道理來(lái)說(shuō)是能夠將 Integer 保存為 Number 的,但由于泛型不變性,List<Integer>并不是List<Number>的子類(lèi)型,所以實(shí)際上該操作將報錯
public static void main(String[] args) { List<Number> numberList = new ArrayList<>(); List<Integer> integerList = new ArrayList<>(); integerList.add(1); integerList.add(2); integerList.add(3); copyAll(numberList, integerList); //報錯 } private static <T> void copyAll(List<T> to, List<T> from) { to.addAll(from); }
思考下該操作為什么會(huì )報錯?
編譯器的作用之一就是進(jìn)行安全檢查并阻止可能發(fā)生不安全行為的操作,copyAll 方法會(huì )報錯,那么肯定就是編譯器覺(jué)得該方法有可能會(huì )觸發(fā)不安全的操作。開(kāi)發(fā)者的本意是希望將 Integer 類(lèi)型的數據轉移到 NumberList 中,只有這種操作且這種操作在我們看來(lái)肯定是安全的,但是編譯器不知道開(kāi)發(fā)者最終所要做的具體操作啊
假設 copyAll方法可以正常調用,那么copyAll方法自然只會(huì )把 from 當做 List<Number>來(lái)看待。因為 Integer 是 Number 的子類(lèi),從 integerList 獲取到的數據對于 numberList 來(lái)說(shuō)自然是安全的。而如果我們在copyAll方法中偷偷向 integerList 傳入了一個(gè) Number 類(lèi)型的值的話(huà),那么自然就將拋出異常,因為 from 實(shí)際上是 List<Integer>類(lèi)型
為了阻止這種不安全的行為,編譯器選擇通過(guò)直接報錯來(lái)進(jìn)行提示。為了解決報錯,我們就需要向編譯器做出安全保證:從 from 取出來(lái)的值只會(huì )當做 Number 類(lèi)型,且不會(huì )向 from 傳入任何值
為了達成以上保證,需要修改下 copyAll 方法
private static <T> void copyAll(List<T> to, List<? extends T> from) { to.addAll(from); }
? extends T 表示 from 接受 T 或者 T 的子類(lèi)型,而不單單是 T 自身,這意味著(zhù)我們可以安全地從 from 中取值并聲明為 T 類(lèi)型,但由于我們并不知道 T 代表的具體類(lèi)型,寫(xiě)入操作并不安全,因此編譯器會(huì )阻止我們向 from 執行傳值操作。有了該限制后,從integerList中取出來(lái)的值只能是當做 Number 類(lèi)型,且避免了向integerList插入非法值的可能,此時(shí)List<Integer>就相當于List<? extends Number>的子類(lèi)型了,從而使得 copyAll 方法可以正常使用
簡(jiǎn)而言之,帶 extends 限定了上界的通配符類(lèi)型使得泛型參數類(lèi)型是協(xié)變的,即如果 A 是 B 的子類(lèi),那么 Generic<A> 就是Generic<? extends B>的子類(lèi)型
協(xié)變所能做到的是:如果 A 是 B 的子類(lèi),那么 Generic<A> 就是Generic<? extends B>的子類(lèi)型。逆變相反,其代表的是:如果 A 是 B 的子類(lèi),那么 Generic<B> 就是 Generic<? super A> 的子類(lèi)型
協(xié)變還比較好理解,畢竟其繼承關(guān)系是相同的,但逆變就比較反直覺(jué)了,整個(gè)繼承關(guān)系都倒過(guò)來(lái)了
逆變的作用可以通過(guò)相同的例子來(lái)理解,copyAll 方法如下修改也可以正常使用,此時(shí)就是向編譯器做出了另一種安全保證:向 numberList 傳遞的值只會(huì )是 Integer 類(lèi)型,且從 numberList 取出的值也只會(huì )當做 Object 類(lèi)型
private static <T> void copyAll(List<? super T> to, List<T> from) { to.addAll(from); }
? super T表示 to 接收 T 或者 T 的父類(lèi)型,而不單單是 T 自身,這意味著(zhù)我們可以安全地向 to 傳類(lèi)型為 T 的值,但由于我們并不知道 T 代表的具體類(lèi)型,所以從 to 取出來(lái)的值只能是 Object 類(lèi)型。有了該限制后,integerList只能向 numberList傳遞類(lèi)型為 Integer 的值,且避免了從 numberList 中獲取到非法類(lèi)型值的可能,此時(shí)List<Number>就相當于List<? super Integer>的子類(lèi)型了,從而使得 copyAll 方法可以正常使用
簡(jiǎn)而言之,帶 super 限定了下界的通配符類(lèi)型使得泛型參數類(lèi)型是逆變的,即如果 A 是 B 的子類(lèi),那么 Generic<B> 就是 Generic<? super A> 的子類(lèi)型
Java 中關(guān)于泛型的困境在 Kotlin 中一樣存在,out 和 in 都是 Kotlin 的關(guān)鍵字,其作用都是為了來(lái)應對泛型問(wèn)題。in 和 out 是一個(gè)對立面,同時(shí)它們又與泛型不變相對立,統稱(chēng)為型變
再來(lái)看下相同例子,該例子在 Java 中存在的問(wèn)題在 Kotlin 中一樣有
fun main() { val numberList = mutableListOf<Number>() val intList = mutableListOf(1, 2, 3, 4) copyAll(numberList, intList) //報錯 numberList.forEach { println(it) } } fun <T> copyAll(to: MutableList<T>, from: MutableList<T>) { to.addAll(from) }
報錯原因和 Java 完全一樣,因為此時(shí)編譯器無(wú)法判斷出我們到底是否會(huì )做出不安全的操作,所以我們依然要來(lái)向編譯器做出安全保證
此時(shí)就需要在 Kotlin 中來(lái)實(shí)現泛型協(xié)變和泛型逆變了,以下兩種方式都可以實(shí)現:
fun <T> copyAll(to: MutableList<T>, from: MutableList<out T>) { to.addAll(from) } fun <T> copyAll(to: MutableList<in T>, from: MutableList<T>) { to.addAll(from) }
out 關(guān)鍵字就相當于 Java 中的<? extends T>,其作用就是限制了 from 不能用于接收值而只能向其取值,這樣就避免了從 to 取出值然后向 from 賦值這種不安全的行為了,即實(shí)現了泛型協(xié)變
in 關(guān)鍵字就相當于 Java 中的<? super T>,其作用就是限制了 to 只能用于接收值而不能向其取值,這樣就避免了從 to 取出值然后向 from 賦值這種不安全的行為了,即實(shí)現了泛型逆變
從這也可以聯(lián)想到,MutableList<*> 就相當于 MutableList<out Any?>了,兩者都帶有相同的限制條件:不允許寫(xiě)值操作,允許讀值操作,且讀取出來(lái)的值只能當做 Any?進(jìn)行處理
在上述例子中,想要實(shí)現協(xié)變還有另外一種方式,那就是使用 List
將 from 的類(lèi)型聲明從 MutableList<T>修改為 List<T> 后,可以發(fā)現 copyAll 方法也可以正常調用了
fun <T> copyAll(to: MutableList<T>, from: List<T>) { to.addAll(from) }
對 Kotlin 有一定了解的同學(xué)應該知道,Kotlin 中的集合框架分為兩種大類(lèi):可讀可寫(xiě)和只能讀不能寫(xiě)
以 Java 中的 ArrayList 為例,Kotlin 將之分為了 MutableList 和 List 兩種類(lèi)型的接口。而 List 接口中的泛型已經(jīng)使用 out 關(guān)鍵字進(jìn)行修飾了,且不包含任何傳入值并保存的方法,即 List 接口只支持讀值而不支持寫(xiě)值,其本身就已經(jīng)滿(mǎn)足了協(xié)變所需要的條件,因此copyAll 方法可以正常使用
public interface List<out E> : Collection<E> { override val size: Int override fun isEmpty(): Boolean override fun contains(element: @UnsafeVariance E): Boolean override fun iterator(): Iterator<E> override fun containsAll(elements: Collection<@UnsafeVariance E>): Boolean public operator fun get(index: Int): E public fun indexOf(element: @UnsafeVariance E): Int public fun lastIndexOf(element: @UnsafeVariance E): Int public fun listIterator(): ListIterator<E> public fun listIterator(index: Int): ListIterator<E> public fun subList(fromIndex: Int, toIndex: Int): List<E> }
雖然 List 接口中有幾個(gè)方法也接收了 E 類(lèi)型的入參參數,但該方法本身不會(huì )進(jìn)行寫(xiě)值操作,所以實(shí)際上可以正常使用,Kotlin 也使用 @UnsafeVariance抑制了編譯器警告
上文講了,由于類(lèi)型擦除,Java 和 Kotlin 的泛型類(lèi)型實(shí)參都會(huì )在編譯階段被擦除,在 Kotlin 中存在一個(gè)額外手段可以來(lái)解決這個(gè)問(wèn)題,即內聯(lián)函數
用關(guān)鍵字 inline 標記的函數就稱(chēng)為內聯(lián)函數,再用 reified 關(guān)鍵字修飾內聯(lián)函數中的泛型形參,編譯器在進(jìn)行編譯的時(shí)候便會(huì )將內聯(lián)函數的字節碼插入到每一個(gè)調用的地方,當中就包括泛型的類(lèi)型實(shí)參。而內聯(lián)函數的類(lèi)型形參能夠被實(shí)化,就意味著(zhù)我們可以在運行時(shí)引用實(shí)際的類(lèi)型實(shí)參了
例如,我們可以寫(xiě)出以下這樣的一個(gè)內聯(lián)函數,用于判斷一個(gè)對象是否是指定類(lèi)型
fun main() { println(1.isInstanceOf<String>()) println("string".isInstanceOf<Int>()) } inline fun <reified T> Any.isInstanceOf(): Boolean { return this is T }
將以上的 Kotlin 代碼反編譯為 Java 代碼,可以看出來(lái) main()方法最終是沒(méi)有調用 isInstanceOf 方法的,具體的判斷邏輯都被插入到了main()方法內部,最終是執行了 instanceof 操作,且指定了具體的泛型類(lèi)型參數 String 和 Integer
public final class GenericTest6Kt { public static final void main() { Object $this$isInstanceOf$iv = 1; int $i$f$isInstanceOf = false; boolean var2 = $this$isInstanceOf$iv instanceof String; $i$f$isInstanceOf = false; System.out.println(var2); Object $this$isInstanceOf$iv = "string"; $i$f$isInstanceOf = false; var2 = $this$isInstanceOf$iv instanceof Integer; $i$f$isInstanceOf = false; System.out.println(var2); } // $FF: synthetic method public static void main(String[] var0) { main(); } // $FF: synthetic method public static final boolean isInstanceOf(Object $this$isInstanceOf) { int $i$f$isInstanceOf = 0; Intrinsics.checkNotNullParameter($this$isInstanceOf, "$this$isInstanceOf"); Intrinsics.reifiedOperationMarker(3, "T"); return $this$isInstanceOf instanceof Object; } }
inline 和 reified 比較有用的一個(gè)場(chǎng)景是用在 Gson 反序列的時(shí)候。由于泛型運行時(shí)類(lèi)型擦除的問(wèn)題,目前用 Gson 反序列化泛型類(lèi)時(shí)步驟是比較繁瑣的,利用 inline 和 reified 我們就可以簡(jiǎn)化很多操作
val gson = Gson() inline fun <reified T> toBean(json: String): T { return gson.fromJson(json, T::class.java) } data class BlogBean(val name: String, val url: String) fun main() { val json = """{"name":"業(yè)志陳","url":"https://juejin.cn/user/923245496518439"}""" val listJson = """[{"name":"業(yè)志陳","url":"https://juejin.cn/user/923245496518439"},{"name":"業(yè)志陳","url":"https://juejin.cn/user/923245496518439"}]""" val blogBean = toBean<BlogBean>(json) val blogMap = toBean<Map<String, String>>(json) val blogBeanList = toBean<List<BlogBean>>(listJson) //BlogBean(name=業(yè)志陳, url=https://juejin.cn/user/923245496518439) println(blogBean) //{name=業(yè)志陳, url=https://juejin.cn/user/923245496518439} println(blogMap) //[{name=業(yè)志陳, url=https://juejin.cn/user/923245496518439}, {name=業(yè)志陳, url=https://juejin.cn/user/923245496518439}] println(blogBeanList) }
我也利用 Kotlin 的這個(gè)強大特性寫(xiě)了一個(gè)用于簡(jiǎn)化 Java / Kotlin 平臺的序列化和反序列化操作的庫:JsonHolder
最后來(lái)做個(gè)簡(jiǎn)單的總結
到此這篇關(guān)于Java和Kotlin的泛型難點(diǎn)的文章就介紹到這了,更多相關(guān)Java Kotlin泛型難點(diǎn)內容請搜索腳本之家以前的文章或繼續瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
免責聲明:本站發(fā)布的內容(圖片、視頻和文字)以原創(chuàng )、來(lái)自互聯(lián)網(wǎng)轉載和分享為主,文章觀(guān)點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權請聯(lián)系QQ:712375056 進(jìn)行舉報,并提供相關(guān)證據,一經(jīng)查實(shí),將立刻刪除涉嫌侵權內容。
Copyright ? 2009-2021 56dr.com. All Rights Reserved. 特網(wǎng)科技 特網(wǎng)云 版權所有 珠海市特網(wǎng)科技有限公司 粵ICP備16109289號
域名注冊服務(wù)機構:阿里云計算有限公司(萬(wàn)網(wǎng)) 域名服務(wù)機構:煙臺帝思普網(wǎng)絡(luò )科技有限公司(DNSPod) CDN服務(wù):阿里云計算有限公司 中國互聯(lián)網(wǎng)舉報中心 增值電信業(yè)務(wù)經(jīng)營(yíng)許可證B2
建議您使用Chrome、Firefox、Edge、IE10及以上版本和360等主流瀏覽器瀏覽本網(wǎng)站