摘要:使用強轉的話,只能強轉成和它的基類,如果強轉成的子類的話,有可能會報運行時異常。擁有類型,它是的子類型因此,我們可以將賦給類型為的變量在聲明處設置后,就可以和或它的子類進行比較了。
歡迎關注我的博客:songjhh"s blog原文連接:對比Java泛型中的extends/super和Kotlin的out/in
在 Java 泛型中,有一個叫做通配符上下界 bounded wildcard 的概念。
extends T>:指的是上界通配符 (Upper Bounded Wildcards)
super T>:指的是下界通配符 (Lower Bounded Wildcards)
相對應在 Kotlin 泛型中,有 out 和 in 兩個關鍵字
下面我將會以工位分配的例子解釋它可以用來解決什么問題,并且對比 Java 來說,Kotlin 作了什么改進。
解決的問題這里有4個實體,分別是 Employee (員工基類),Manager (經理), DevManager (開發經理),WorkStation 工位。
它們的關系如下:
@Data public class Employee { private String name; public Employee(String name) { this.name = name; } } @Data public class Manager extends Employee { private Integer level; public Manager(String name) { super(name); } } @Data public class DevManager extends Manager { private String language; public DevManager(String name) { super(name); } }
其中一個工位可以坐一個員工, 這里用泛型抽象出員工來:
@Data public class WorkStation{ private T employee; public WorkStation(T employee) { this.employee = employee; } }
按照邏輯,一個經理的工位,當然也是一個員工的工位,但事實真的如此嗎?
// 創建一個經理工位 WorkStationmanagerWorkStation = new WorkStation<>(new Manager("John")); // 將經理工位賦給員工工位 WorkStation employWorkStation = managerWorkStation; // error
但這里會報 incompatible types: WorkStation
造成這個現象的原因,是因為Java 的參數類型是不型變的 invariant,而通配符上下界正是為了繞過這個問題。
ps: 型變在計算機編程中,特別是面向對象編程,是重要的基石,可以在測試階段幫助程序員發現很多的錯誤,這里不展開討論。有界限的通配符(Bounded Wildcards)
為了幫助理解和記憶,在講通配符上下界之前,這里先講一講PECS原則。
PECS stands for producer-extends, consumer-superFrom: Effective Java Third Edition - Item 31
這里引用的是 Effective Java Third Edition 關于如何利用 bounded wildcards 來提升 API 靈活性章節一個助記詞。
簡單來說,生產者適合用 extends T>,而消費者適合用 super T>,這里生產者指的是能用來讀取的對象,消費者指的是用來寫入的對象,下面將會詳細解釋這兩個概念。
上界通配符(extends)還是接著上面的例子,員工的工位為了獲得經理工位的引用,這里使用上界通配符 extends T>
// 創建一個經理工位 WorkStationmanagerWorkStation = new WorkStation<>(new Manager("John")); // 將經理工位的引用賦給一個繼承于員工對象的工位 WorkStation extends Employee> exWorkStation = managerWorkStation;
可以看到使用了上界通配符,我們將經理工位和員工工位關聯起來了,使得 Java 泛型的靈活性大大增加。
但是上面介紹了 PECS原則 , 它指出上界通配符只適合用于生產者中,下面我帶大家來看看這句話如何理解:
WorkStationmanagerWorkStation = new WorkStation<>(new Manager("John")); WorkStation extends Employee> exWorkStation = managerWorkStation; // 只可以獲取它和它的基類 Object a = exWorkStation.getEmployee(); Employee b = exWorkStation.getEmployee(); DevManager d = exWorkStation.getEmployee(); // error // 不可以存儲 exWorkStation.setEmployee(new Employee("Sam")); // error, incompatible types: Manager cannot be coverted to capture#1 of ? extends Employee exWorkStation.setEmployee(new DevManager("James")); // error, incompatible types: DevManager cannot be coverted to capture#1 of ? extends Employee
上面的例子可以看到,使用了上界通配符只能用 get() 方法取出工位占位的類型和其基類,但是不能再用 set() 方法存對象到工位中,所以說上界通配符只適合用于生產者中。
原因也很好理解,因為編譯器只知道工位坐的人是 Employee 對象或它的派生類,但不知道具體是哪個對象(編譯器用 capture#1 標記占位,指這里捕獲 Employee 和它的子類),所以不能夠判斷存入的對象是不是這個工位能夠匹配的:
坐在 exWorkStation 的人一定是一個員工,所以可以取出 Employee
exWorkStation 可能是 Manager 的工位,所以這里存取 TestManager 是沒問題的。但問題在于它也可能是 DevManager 的工位,那么 TestManager 就不能坐在這個工位里了,編輯器無法判斷,所以上界通配符不能用 set() 方法
簡而言之,上界通配符 Upper Bounded Wildcards 使得參數類型是協變的covariant。
下界通配符和上界通配符恰恰相反,下界通配符 super T> 適合存儲對象的場景。
WorkStation super Manager> supWorkStation = new WorkStation<>(new Manager("James")); // 可以存儲它和它的子類 supWorkStation.setEmployee(new DevManager("Sam")); supWorkStation.setEmployee(new Manager("Sam")); supWorkStation.setEmployee(new Employee("Sam")); // error // 只可以獲取所有類的基類 - Object Object o = supWorkStation.getEmployee(); Employee e = supWorkStation.getEmployee(); // error Manager e = supWorkStation.getEmployee(); // error DevManager e = supWorkStation.getEmployee(); // error // 只能安全強轉成它和它的基類 Employee employee = (Employee) o; Manager manager = (Manager) o; WorkStation super Manager> w = new WorkStation<>(new Manager("Sam")); // ClassCastException: Manager cannot be cast to DevManager DevManager devManager = (DevManager) w.getEmployee();
上面的例子可以看到,使用下界通配符可以用 set() 方法儲存 Manager 和其子類,但只能用 get() 方法獲得所有類的基類 Object 對象。使用強轉的話,只能強轉成 Manager 和它的基類,如果強轉成 Manager 的子類的話,有可能會報 ClassCastException 運行時異常。
因為存入方便,取出數據比較麻煩,所以說下界通配符適合使用在消費者中。
究其原因,可以簡單理解為,下界通配符標記了該工位至少是 Manager 的工位,所以這里無論是坐 DevManager 還是 TestManager 都沒有問題。
這個就叫做逆變性(contravariance)。
在Kotlin的世界里是怎么樣的?是 Java 世界是用通配符上下界來覺得泛型不型變的,那在 Kotlin 是怎么樣的呢?
val managerWorkStation: WorkStation= WorkStation(Manager("John")) val station: WorkStation = managerWorkStation // error, type mismatch
由此看到在 Kotlin 里對泛型也是有限制的。相對于 Java 提供的 extends T> 和 super T>,Kotlin 相對應提供了 out 和 in 關鍵字。
在 Kotlin 中 out 相當于 extends T>,in 相當于 super T>,這里看看用法。
out 關鍵字:
val managerWorkStation: WorkStation= WorkStation(Manager("John")) val outStation: WorkStation = managerWorkStation // 只可以獲取它和它的基類 val a: Any = outStation.employee val b: Employee = outStation.employee val c: Employee = managerWorkStation.employee val d: DevManager = managerWorkStation.employee // error, type mismatch // 不可以存儲 outStation.employee = DevManager("Sam") // Setter for "employee" is removed by type projection
in關鍵字:
val inStation: WorkStation= WorkStation() // 可以存儲它和它的子類 inStation.employee = Manager("James") inStation.employee = DevManager("James") inStation.employee = Employee("James") // error, type mismatch // 只可以獲得Any val any: Any? = inStation.employee // 只能安全強轉成它和它的基類 val employee: Employee = any as Employee val manager:Manager = any as Manager
由以上兩個例子可以看到,Kotlin 和 Java 非常相似,只是相關的關鍵字有所不同而已。但畢竟 Kotlin 是號稱要解決 Java 的,那么會不會哪里有所不同呢?
Kotlin 和 Java 的異同 使用處型變在 Java 中,上下界通配符只能用在參數、屬性、變量或者返回值中,不能在泛型聲明處使用,所以才叫做使用處型變。
以上的 Kotlin 例子也用的是使用處型變,被稱為類型投影。
所以 Java 和 Kotlin 都提供使用處型變。
聲明處型變但不同的是,Kotlin 還提供 Java 所不具備的聲明處型變。
顧名思義,Kotlin 提供的 out 和 in 兩個型變關鍵字還可以用于泛型聲明的時候。
public interface Collection: Iterable { ... } // 錯誤,這里只能用val,不能用var class Source (var t: T) { ... }
在聲明處設置 out 后,使得了在 Kotlin 中,Collection
interface Comparable{ operator fun compareTo(other: T): Int } fun demo(x: Comparable ) { x.compareTo(1.0) // 1.0 擁有類型 Double,它是 Number 的子類型 // 因此,我們可以將 x 賦給類型為 Comparable 的變量 val y: Comparable = x }
Comparable 在聲明處設置 in 后,x 就可以和 Number 或它的子類進行比較了。
總結以上就是 Java 和 Kotlin 關于泛型型變的內容,其中 Kotlin 對比 Java,多加了聲明處型變的方式。
Java | Java示例代碼 | Kotlin示例代碼 |
---|---|---|
使用處型變 | void example(List extends Number> list) | fun example(list: List |
使用處逆變 | void example(List super Integer>) | fun example(list: List |
聲明處型變 | - | interface Collection |
聲明處逆變 | - | interface Comparable |
為了幫助記憶,上文引用了PECS原則:producer-extends, consumer-super。
最后這里再引用Effective Java - 31 | Use bounded wildcards to increase API flexibilty里面對通配符的幾個意見:
If an input parameter is both a producer and a consumer, then wildcard types will do you no good.
如果輸入參數同時是生產者和消費者, 那么通配符對你來說不是一個好的選擇。
Do not use bounded wildcard types as return types, if the user of a class has to think about wildcard types, there is probably something wrong with its API.
不要用界限通配符作為你的返回類型,如果類的用戶必須考慮通配符類型,類的 API 或許就會出錯。
If a type parameter appears only once in a method declaration, replace it with a wildcard.
如果類型參數只在方法聲明中出現一次,就可以用通配符取代它。
謝謝閱讀
版權聲明:歡迎轉載 (http://songjhh.top/2019/03/13...
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/73735.html
摘要:知識點總結泛型知識點總結泛型泛型泛型就是參數化類型適用于多種數據類型執行相同的代碼泛型中的類型在使用時指定泛型歸根到底就是模版優點使用泛型時,在實際使用之前類型就已經確定了,不需要強制類型轉換。 Java知識點總結(Java泛型) @(Java知識點總結)[Java, Java泛型] [toc] 泛型 泛型就是參數化類型 適用于多種數據類型執行相同的代碼 泛型中的類型在使用時指定 泛...
摘要:百度網盤提取碼一面試題熟練掌握是很關鍵的,大公司不僅僅要求你會使用幾個,更多的是要你熟悉源碼實現原理,甚至要你知道有哪些不足,怎么改進,還有一些有關的一些算法,設計模式等等。 ??百度網盤??提取碼:u6C4?一、java面試題熟練掌握java是很關鍵的,大公司不僅僅要求你會使用幾個api,更多的是要你熟悉源碼實現原理,甚...
摘要:第章元編程與注解反射反射是在運行時獲取類的函數方法屬性父類接口注解元數據泛型信息等類的內部信息的機制。本章介紹中的注解與反射編程的相關內容。元編程本質上是一種對源代碼本身進行高層次抽象的編碼技術。反射是促進元編程的一種很有價值的語言特性。 第12章 元編程與注解、反射 反射(Reflection)是在運行時獲取類的函數(方法)、屬性、父類、接口、注解元數據、泛型信息等類的內部信息的機...
摘要:虛擬機中并沒有泛型類型對象,所有的對象都是普通類。其原因就是泛型的擦除。中數組是協變的,泛型是不可變的。在不指定泛型的情況下,泛型變量的類型為該方法中的幾種類型的同一個父類的最小級,直到。 引入泛型的主要目標有以下幾點: 類型安全 泛型的主要目標是提高 Java 程序的類型安全 編譯時期就可以檢查出因 Java 類型不正確導致的 ClassCastException 異常 符合越早出...
摘要:好了,有了這樣的背景知識,我們可以來看一下上界通配了,在中,可以使用來界定一個上界,的意思是所有屬于的子類,是上界,不能突破天界啊,我們具體化一下,的意思就是,所有的子類都可以匹配這個通配符。 1、上界通配符 首先,需要知道的是,Java語言中的數組是支付協變的,什么意思呢?看下面的代碼: static class A extends Base{ void f(...
閱讀 1735·2023-04-25 19:37
閱讀 1298·2021-11-16 11:45
閱讀 2802·2021-10-18 13:30
閱讀 2763·2021-09-29 09:34
閱讀 1616·2019-08-30 15:55
閱讀 3110·2019-08-30 11:10
閱讀 1833·2019-08-29 16:52
閱讀 994·2019-08-29 13:18