您好,登錄后才能下訂單哦!
詳解Java和Kotlin中的泛型?相信很多沒有經驗的人對此束手無策,為此本文總結了問題出現的原因和解決方法,通過這篇文章希望你能解決這個問題。
泛型允許你定義帶類型形參的數據類型,當這種類型的實例被創建出來后,類型形參便被替換為稱為類型實參的具體類型。例如,對于 List<T>,List 稱為基礎類型,T 便是類型型參,T 可以是任意類型,當沒有指定 T 的具體類型時,我們只能知道List<T>是一個集合列表,但不知道承載的具體數據類型。而對于 List<String>,當中的 String 便是類型實參,我們可以明白地知道該列表承載的都是字符串,在這里 String 就相當于一個參數傳遞給了 List,在這語義下 String 也稱為類型參數
此外,在 Kotlin 中我們可以實現實化類型參數,在運行時的內聯函數中拿到作為類型實參的具體類型,即可以實現 T::class.java,但在 Java 中卻無法實現,因為內聯函數是 Kotlin 中的概念,Java 中并不存在
泛型是在 Java 5 版本開始引入的,先通過幾個小例子來明白泛型的重要性
以下代碼可以成功編譯,但是在運行時卻拋出了 ClassCastException。了解 ArrayList 源碼的同學就知道其內部是用一個Object[]數組來存儲數據的,這使得 ArrayList 能夠存儲任何類型的對象,所以在沒有泛型的年代開發者一不小心就有可能向 ArrayList 存入了非期望值,編譯期完全正常,等到在運行時就會拋出類型轉換異常了
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
而有了泛型后,我們就可以寫出更加健壯安全的代碼,以下錯誤就完全可以在編譯階段被發現,且取值的時候也不需要進行類型強轉
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); }
此外,利用泛型我們可以寫出更加具備通用性的代碼。例如,假設我們需要從一個 List 中篩選出大于 0 的全部數字,那我們自然不想為 Integer、Float、Double 等多種類型各寫一個篩選方法,此時就可以利用泛型來抽象篩選邏輯
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; }
總的來說,泛型有以下幾點優勢:
類型檢查,在編譯階段就能發現錯誤
更加語義化,看到 List<String>我們就知道存儲的數據類型是 String
自動類型轉換,在取值時無需進行手動類型轉換
能夠將邏輯抽象出來,使得代碼更加具有通用性
泛型是在 Java 5 版本開始引入的,所以在 Java 4 中 ArrayList 還不屬于泛型類,其內部通過 Object 向上轉型和外部強制類型轉換來實現數據存儲和邏輯復用,此時開發者的項目中已經充斥了大量以下類型的代碼:
List stringList = new ArrayList(); stringList.add("業志陳"); stringList.add("https://juejin.cn/user/923245496518439"); String str = (String) stringList.get(0);
而在推出泛型的同時,Java 官方也必須保證二進制的向后兼容性,用 Java 4 編譯出的 Class 文件也必須能夠在 Java 5 上正常運行,即 Java 5 必須保證以下兩種類型的代碼能夠在 Java 5 上共存且正常運行
List stringList = new ArrayList(); List<String> stringList = new ArrayList();
為了實現這一目的,Java 就通過類型擦除這種比較別扭的方式來實現泛型。編譯器在編譯時會擦除類型實參,在運行時不存在任何類型相關的信息,泛型對于 JVM 來說是透明的,有泛型和沒有泛型的代碼通過編譯器編譯后所生成的二進制代碼是完全相同的
例如,分別聲明兩個泛型類和非泛型類,拿到其 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("業志陳"); NodeB<String> nodeB = new NodeB<>("業志陳"); System.out.println(nodeB.obj); } }
可以看到 NodeA 和 NodeB 兩個對象對應的字節碼其實是完全一樣的,最終都是使用 Object 來承載數據,就好像傳遞給 NodeB 的類型參數 String 不見了一樣,這便是類型擦除
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 業志陳 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 業志陳 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 類型,并且為泛型類 NodeB 設定上界約束 String,兩者的字節碼也會完全一樣
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("業志陳"); NodeB<String> nodeB = new NodeB<>("業志陳"); 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 業志陳 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 業志陳 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 }
所以說,當泛型類型被擦除后有兩種轉換方式
如果泛型沒有設置上界約束,那么將泛型轉化成 Object 類型
如果泛型設置了上界約束,那么將泛型轉化成該上界約束
該結論也可以通過反射泛型類的 Class 對象來驗證
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<>("業志陳"); 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
那既然在運行時不存在任何類型相關的信息,泛型又為什么能夠實現類型檢查和類型自動轉換等功能呢?
其實,類型檢查是編譯器在編譯前幫我們完成的,編譯器知道我們聲明的具體的類型實參,所以類型擦除并不影響類型檢查功能。而類型自動轉換其實是通過內部強制類型轉換來實現的,上面給出的字節碼中也可以看到有一條類型強轉 checkcast 的語句
27: checkcast #9 // class java/lang/String
例如,ArrayList 內部雖然用于存儲數據的是 Object 數組,但 get 方法內部會自動完成類型強轉
transient Object[] elementData; public E get(int index) { rangeCheck(index); return elementData(index); } @SuppressWarnings("unchecked") E elementData(int index) { //強制類型轉換 return (E) elementData[index]; }
所以 Java 的泛型可以看做是一種特殊的語法糖,因此也被人稱為偽泛型
Java 泛型對于類型的約束只在編譯期存在,運行時仍然會按照 Java 5 之前的機制來運行,泛型的具體類型在運行時已經被刪除了,所以 JVM 是識別不到我們在代碼中指定的具體的泛型類型的
例如,雖然List<String>只能用于添加字符串,但我們只能泛化地識別到它屬于List<?>類型,而無法具體判斷出該 List 內部包含的具體類型
List<String> stringList = new ArrayList<>(); //正常 if (stringList instanceof ArrayList<?>) { } //報錯 if (stringList instanceof ArrayList<String>) { }
我們只能對具體的對象實例進行類型校驗,但無法判斷出泛型形參的具體類型
public <T> void filter(T data) { //正常 if (data instanceof String) { } //報錯 if (T instanceof String) { } //報錯 Class<T> tClass = T::getClass; }
此外,類型擦除也會導致 Java 中出現多態問題。例如,以下兩個方法的方法簽名并不完全相同,但由于類型擦除的原因,入參參數的數據類型都會被看成 List<Object>,從而導致兩者無法共存在同一個區域內
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()) //泛型類型自動推導 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 }
需要注意的是,為了實現向后兼容,目前高版本 Java 依然允許實例化沒有具體類型參數的泛型類,這可以說是一個對新版本 JDK 危險但對舊版本友好的兼容措施。但 Kotlin 要求在使用泛型時需要顯式聲明泛型類型或者是編譯器能夠類型推導出具體類型,任何不具備具體泛型類型的泛型類都無法被實例化。因為 Kotlin 一開始就是基于 Java 6 版本的,一開始就存在了泛型,自然就不存在需要兼容老代碼的問題,因此以下例子和 Java 會有不同的表現
val arrayList1 = ArrayList() //錯誤,編譯器報錯 val arrayList2 = arrayListOf<Int>() //正常 val arrayList3 = arrayListOf(1, 2, 3) //正常
還有一個比較容易讓人誤解的點。我們經常會使用 as 和 as? 來進行類型轉換,但如果轉換對象是泛型類型的話,那就會由于類型擦除而出現誤判。如果轉換對象有正確的基礎類型,那么轉換就會成功,而不管類型實參是否相符。因為在運行時轉換發生的時候類型實參是未知的,此時編譯器只會發出 “unchecked cast” 警告,代碼還是可以正常編譯的
例如,在以下例子中代碼的運行結果還符合我們的預知。第一個轉換操作由于類型相符,所以打印出了相加值。第二個轉換操作由于基礎類型是 Set 而非 List,所以拋出了 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,這是因為在運行時不會判斷且無法判斷出類型實參到底是否是 Int,而只會判斷基礎類型 List 是否相符,所以 as? 操作會成功,等到要執行相加操作時才會發現拿到的是 String 而非 Number
printSum(listOf("1", "2", "3")) Exception in thread "main" java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Number
泛型本身已經帶有類型約束的作用,我們也可以進一步細化其支持的具體類型
例如,假設存在一個盤子 Plate,我們要求該 Plate 只能用于裝水果 Fruit,那么就可以對其泛型聲明做進一步約束,Java 中使用 extend 關鍵字來聲明約束規則,而 Kotlin 使用的是 : 。這樣 Plate 就只能用于 Fruit 和其子類,而無法用于 Noodles 等不相關的類型,這種類型約束就被稱為上界約束
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()) //報錯 }
如果上界約束擁有多層類型元素,Java 是使用 & 符號進行鏈式聲明,Kotlin 則是用 where 關鍵字來依次進行聲明
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()) //正常 }
此外,沒有指定上界約束的類型形參會默認使用 Any? 作為上界,即我們可以使用 String 或 String? 作為具體的類型實參。如果想確保最終的類型實參一定是非空類型,那么就需要主動聲明上界約束為 Any
假設現在有個需求,需要我們提供一個方法用于遍歷所有類型的 List 集合并打印元素
第一種做法就是直接將方法參數類型聲明為 List,不包含任何泛型類型聲明。這種做法可行,但編譯器會警告無法確定 list元素的具體類型,所以這不是最優解法
public static void printList1(List list) { for (Object o : list) { System.out.println(o); } }
可能會想到的第二種做法是:將泛型類型直接聲明為 Object,希望讓其適用于任何類型的 List。這種做法完全不可行,因為即使 String 是 Object 的子類,但 List<String> 和 List<Object>并不具備從屬關系,這導致 printList2 方法實際上只能用于List<Object>這一種具體類型
public static void printList2(List<Object> list) { for (Object o : list) { System.out.println(o); } }
最優解法就是要用到 Java 的類型通配符 ? 了,printList3方法完全可行且編譯器也不會警告報錯
public static void printList3(List<?> list) { for (Object o : list) { System.out.println(o); } }
? 表示我們并不關心具體的泛型類型,而只是想配合其它類型進行一些條件限制。例如,printList3方法希望傳入的是一個 List,但不限制泛型的具體類型,此時List<?>就達到了這一層限制條件
類型通配符也存在著一些限制。因為 printList3 方法并不包含具體的泛型類型,所以我們從中取出的值只能是 Object 類型,且無法向其插入值,這都是為了避免發生 ClassCastException
Java 的類型通配符對應 Kotlin 中的概念就是**星號投影 * **,Java 存在的限制在 Kotlin 中一樣有
fun printList(list: List<*>) { for (any in list) { println(any) } }
此外,星號投影只能出現在類型形參的位置,不能作為類型實參
val list: MutableList<*> = ArrayList<Number>() //正常 val list2: MutableList<*> = ArrayList<*>() //報錯
看以下例子。Apple 和 Banana 都是 Fruit 的子類,可以發現 Apple[] 類型的對象是可以賦值給 Fruit[] 的,且 Fruit[] 可以容納 Apple 對象和 Banana 對象,這種設計就被稱為協變,即如果 A 是 B 的子類,那么 A[] 就是 B[] 的子類型。相對的,Object[] 就是所有數組對象的父類型
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(); //編譯時正常,運行時拋出 ArrayStoreException fruitArray[1] = new Banana(); }
而 Java 中的泛型是不變的,這意味著 String 雖然是 Object 的子類,但List<String>并不是List<Object>的子類型,兩者并不具備繼承關系
List<String> stringList = new ArrayList<>(); List<Object> objectList = stringList; //報錯
那為什么 Java 中的泛型是不變的呢?
這可以通過看一個例子來解釋。假設 Java 中的泛型是協變的,那么以下代碼就可以成功通過編譯階段的檢查,在運行時就不可避免地將拋出 ClassCastException,而引入泛型的初衷就是為了實現類型安全,支持協變的話那泛型也就沒有比數組安全多少了,因此就將泛型被設計為不變的
List<String> strList = new ArrayList<>(); List<Object> objs = strList; //假設可以運行,實際上編譯器會報錯 objs.add(1); String str = strList.get(0); //將拋出 ClassCastException,無法將整數轉換為字符串
再來想個問題,既然協變本身并不安全,那么數組為何又要被設計為協變呢?
Arrays 類包含一個 equals方法用于比較兩個數組對象是否相等。如果數組是協變的,那么就需要為每一種數組對象都定義一個 equals方法,包括開發者自定義的數據類型。想要避免這種情況,就需要讓 Object[] 可以接收任意數組類型,即讓 Object[] 成為所有數組對象的父類型,這就使得數組必須支持協變,這樣多態才能生效
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 數組并不支持協變,Kotlin 數組類似于集合框架,具有對應的實現類 Array,Array 屬于泛型類,支持了泛型因此也不再協變
val stringArray = arrayOfNulls<String>(3) val anyArray: Array<Any?> = stringArray //報錯
Java 的泛型也并非完全不變的,只是實現協變需要滿足一些條件,甚至也可以實現逆變,下面就來介紹下泛型如何實現協變和逆變
假設我們定義了一個copyAll希望用于 List 數據遷移。那以下操作在我們看來就是完全安全的,因為 Integer 是 Number 的子類,按道理來說是能夠將 Integer 保存為 Number 的,但由于泛型不變性,List<Integer>并不是List<Number>的子類型,所以實際上該操作將報錯
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); }
思考下該操作為什么會報錯?
編譯器的作用之一就是進行安全檢查并阻止可能發生不安全行為的操作,copyAll 方法會報錯,那么肯定就是編譯器覺得該方法有可能會觸發不安全的操作。開發者的本意是希望將 Integer 類型的數據轉移到 NumberList 中,只有這種操作且這種操作在我們看來肯定是安全的,但是編譯器不知道開發者最終所要做的具體操作啊
假設 copyAll方法可以正常調用,那么copyAll方法自然只會把 from 當做 List<Number>來看待。因為 Integer 是 Number 的子類,從 integerList 獲取到的數據對于 numberList 來說自然是安全的。而如果我們在copyAll方法中偷偷向 integerList 傳入了一個 Number 類型的值的話,那么自然就將拋出異常,因為 from 實際上是 List<Integer>類型
為了阻止這種不安全的行為,編譯器選擇通過直接報錯來進行提示。為了解決報錯,我們就需要向編譯器做出安全保證:從 from 取出來的值只會當做 Number 類型,且不會向 from 傳入任何值
為了達成以上保證,需要修改下 copyAll 方法
private static <T> void copyAll(List<T> to, List<? extends T> from) { to.addAll(from); }
? extends T 表示 from 接受 T 或者 T 的子類型,而不單單是 T 自身,這意味著我們可以安全地從 from 中取值并聲明為 T 類型,但由于我們并不知道 T 代表的具體類型,寫入操作并不安全,因此編譯器會阻止我們向 from 執行傳值操作。有了該限制后,從integerList中取出來的值只能是當做 Number 類型,且避免了向integerList插入非法值的可能,此時List<Integer>就相當于List<? extends Number>的子類型了,從而使得 copyAll 方法可以正常使用
簡而言之,帶 extends 限定了上界的通配符類型使得泛型參數類型是協變的,即如果 A 是 B 的子類,那么 Generic<A> 就是Generic<? extends B>的子類型
協變所能做到的是:如果 A 是 B 的子類,那么 Generic<A> 就是Generic<? extends B>的子類型。逆變相反,其代表的是:如果 A 是 B 的子類,那么 Generic<B> 就是 Generic<? super A> 的子類型
協變還比較好理解,畢竟其繼承關系是相同的,但逆變就比較反直覺了,整個繼承關系都倒過來了
逆變的作用可以通過相同的例子來理解,copyAll 方法如下修改也可以正常使用,此時就是向編譯器做出了另一種安全保證:向 numberList 傳遞的值只會是 Integer 類型,且從 numberList 取出的值也只會當做 Object 類型
private static <T> void copyAll(List<? super T> to, List<T> from) { to.addAll(from); }
? super T表示 to 接收 T 或者 T 的父類型,而不單單是 T 自身,這意味著我們可以安全地向 to 傳類型為 T 的值,但由于我們并不知道 T 代表的具體類型,所以從 to 取出來的值只能是 Object 類型。有了該限制后,integerList只能向 numberList傳遞類型為 Integer 的值,且避免了從 numberList 中獲取到非法類型值的可能,此時List<Number>就相當于List<? super Integer>的子類型了,從而使得 copyAll 方法可以正常使用
簡而言之,帶 super 限定了下界的通配符類型使得泛型參數類型是逆變的,即如果 A 是 B 的子類,那么 Generic<B> 就是 Generic<? super A> 的子類型
Java 中關于泛型的困境在 Kotlin 中一樣存在,out 和 in 都是 Kotlin 的關鍵字,其作用都是為了來應對泛型問題。in 和 out 是一個對立面,同時它們又與泛型不變相對立,統稱為型變
out 本身帶有出去的意思,本身帶有傾向于取值操作的意思,用于泛型協變
in 本身帶有進來的意思,本身帶有傾向于傳值操作的意思,用于泛型逆變
再來看下相同例子,該例子在 Java 中存在的問題在 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 完全一樣,因為此時編譯器無法判斷出我們到底是否會做出不安全的操作,所以我們依然要來向編譯器做出安全保證
此時就需要在 Kotlin 中來實現泛型協變和泛型逆變了,以下兩種方式都可以實現:
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 關鍵字就相當于 Java 中的<? extends T>,其作用就是限制了 from 不能用于接收值而只能向其取值,這樣就避免了從 to 取出值然后向 from 賦值這種不安全的行為了,即實現了泛型協變
in 關鍵字就相當于 Java 中的<? super T>,其作用就是限制了 to 只能用于接收值而不能向其取值,這樣就避免了從 to 取出值然后向 from 賦值這種不安全的行為了,即實現了泛型逆變
從這也可以聯想到,MutableList<*> 就相當于 MutableList<out Any?>了,兩者都帶有相同的限制條件:不允許寫值操作,允許讀值操作,且讀取出來的值只能當做 Any?進行處理
在上述例子中,想要實現協變還有另外一種方式,那就是使用 List
將 from 的類型聲明從 MutableList<T>修改為 List<T> 后,可以發現 copyAll 方法也可以正常調用了
fun <T> copyAll(to: MutableList<T>, from: List<T>) { to.addAll(from) }
對 Kotlin 有一定了解的同學應該知道,Kotlin 中的集合框架分為兩種大類:可讀可寫和只能讀不能寫
以 Java 中的 ArrayList 為例,Kotlin 將之分為了 MutableList 和 List 兩種類型的接口。而 List 接口中的泛型已經使用 out 關鍵字進行修飾了,且不包含任何傳入值并保存的方法,即 List 接口只支持讀值而不支持寫值,其本身就已經滿足了協變所需要的條件,因此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 接口中有幾個方法也接收了 E 類型的入參參數,但該方法本身不會進行寫值操作,所以實際上可以正常使用,Kotlin 也使用 @UnsafeVariance抑制了編譯器警告
上文講了,由于類型擦除,Java 和 Kotlin 的泛型類型實參都會在編譯階段被擦除,在 Kotlin 中存在一個額外手段可以來解決這個問題,即內聯函數
用關鍵字 inline 標記的函數就稱為內聯函數,再用 reified 關鍵字修飾內聯函數中的泛型形參,編譯器在進行編譯的時候便會將內聯函數的字節碼插入到每一個調用的地方,當中就包括泛型的類型實參。而內聯函數的類型形參能夠被實化,就意味著我們可以在運行時引用實際的類型實參了
例如,我們可以寫出以下這樣的一個內聯函數,用于判斷一個對象是否是指定類型
fun main() { println(1.isInstanceOf<String>()) println("string".isInstanceOf<Int>()) } inline fun <reified T> Any.isInstanceOf(): Boolean { return this is T }
將以上的 Kotlin 代碼反編譯為 Java 代碼,可以看出來 main()方法最終是沒有調用 isInstanceOf 方法的,具體的判斷邏輯都被插入到了main()方法內部,最終是執行了 instanceof 操作,且指定了具體的泛型類型參數 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 比較有用的一個場景是用在 Gson 反序列的時候。由于泛型運行時類型擦除的問題,目前用 Gson 反序列化泛型類時步驟是比較繁瑣的,利用 inline 和 reified 我們就可以簡化很多操作
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":"業志陳","url":"https://juejin.cn/user/923245496518439"}""" val listJson = """[{"name":"業志陳","url":"https://juejin.cn/user/923245496518439"},{"name":"業志陳","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=業志陳, url=https://juejin.cn/user/923245496518439) println(blogBean) //{name=業志陳, url=https://juejin.cn/user/923245496518439} println(blogMap) //[{name=業志陳, url=https://juejin.cn/user/923245496518439}, {name=業志陳, url=https://juejin.cn/user/923245496518439}] println(blogBeanList) }
我也利用 Kotlin 的這個強大特性寫了一個用于簡化 Java / Kotlin 平臺的序列化和反序列化操作的庫:JsonHolder
最后來做個簡單的總結
協變 | 逆變 | 不變 | |
---|---|---|---|
Kotlin | <out T> ,只能作為消費者,只能讀取不能添加 | <in T> ,只能作為生產者,只能添加,讀取出的值只能當做 Any 類型 | <T> ,既可以添加也可以讀取 |
Java | <? extends T> ,只能作為消費者,只能讀取不能添加 | <? super T> ,只能作為生產者,只能添加,讀取出的值只能當做 Object 類型 | <T> ,既可以添加也可以讀取 |
看完上述內容,你們掌握詳解Java和Kotlin中的泛型的方法了嗎?如果還想學到更多技能或想了解更多相關內容,歡迎關注億速云行業資訊頻道,感謝各位的閱讀!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。