上期主要分享了 From Java To Kotlin 1 :空安全、擴展、函式、Lambda,
這是 From Java to Kotlin 第二期, From Java to Kotlin 關鍵在于 思維的轉變,
運算式思維
Kotlin 中大部分陳述句是運算式, 運算式思維是一種編程思維, 編程思維是一種非常抽象的概念,很多時候是只可意會不可言傳的, 不過,從某種程度上看,學習編程思維,比學習編程語法更重要,因為編程思維決定著我們的代碼整體的架構與風格,而具體的某個語法反而沒那么大的影響力,當然,如果對 Kotlin 的語法沒有一個全面的認識,編程思維也只會是空中樓閣,就像,我們學會了基礎的漢字以后開始寫作文:學了漢字以后,如果沒掌握寫作的技巧,是寫不出好的文章的,同理,如果學了 Kotlin 語法,卻沒有掌握它的編程思維,也是寫不出優雅的 Kotlin 代碼的,
下面我們看一段 Kotlin 代碼
//--- 1
var i = 0
if (data != null) {
i = data
}
//--- 2
var j = 0
if (data != null) {
j = data
} else {
j = getDefault()
println(j)
}
//--- 3
var k = 0
if (data != null) {
k = data
} else {
throw NullPointerException()
}
//--- 4
var x = 0
when (data) {
is Int -> x = data
else -> x = 0
}
//--- 5
var y = 0
try {
y = "Kotlin".toInt()
} catch (e: NumberFormatException) {
println(e)
y = 0
}
這些代碼,如果我們用平時寫 Java 時的思維來分析的話,是挑不出太多毛病的,但是站在 Kotlin 的角度,就完全不一樣了,利用 Kotlin 的語法,我們完全可以將代碼寫得更加簡潔,就像下面這樣:
//--- 1
val i = data ?: 0
//--- 2
val j = data ?: getDefault().also { println(it) }
//--- 3
val k = data?: throw NullPointerException()
//--- 4
val x = when (data) {
is Int -> data
else -> 0
}
//--- 5
val y = try {
"Kotlin".toInt()
} catch (e: NumberFormatException) {
println(e)
0
}
這段代碼看起來就簡潔了不少,所以從 Java 轉到 Kotlin 要格外注意思維轉變,培養運算式思維,
這里有個疑問:Kotlin 為什么就能用這樣的方式寫代碼呢?其實這是因為:if、when、throw、try-catch 這些語法,在 Kotlin 當中都是運算式,
那么,這個“運算式”到底是什么呢?其實,與運算式(Expression)對應的,還有另一個概念,我們叫做陳述句(Statement),
-
運算式(Expression),是一段可以產生值的代碼;
-
陳述句(Statement),則是一句不產生值的代碼,
我們可以簡單來概括一下:運算式(Expression)有值,而陳述句(Statement)不總有,
用一個更詳細的例子解釋:
val a = 1 // statement
println(a) // statement
// statement
var i = 0
if (data != null) {
i = data
}
// 1 + 2 是一個運算式,但是對b的賦值行為是statement
val b = 1 + 2
// if else 整體是一個運算式
// a > b是一個運算式, 子運算式
// a - b是一個運算式, 子運算式
// b - a是一個運算式, 子運算式,
fun minus(a: Int, b: Int) = if (a > b) a - b else b - a
// throw NotImplementedError() 是一個運算式
fun calculate(): Int = throw NotImplementedError()
這段代碼是描述了常見的 Kotlin 代碼模式,從它的注釋當中,我們其實可以總結出這樣幾個規律:
-
賦值陳述句,就是典型的 statement;
-
if 語法,既可以作為陳述句,也可以作為運算式;
-
陳述句與運算式,它們可能會出現在同一行代碼中,比如 val b = 1 + 2;
-
運算式還可能包含“子運算式”,就比如這里的 minus 方法;
-
throw 陳述句,也可以作為運算式,
看到這里,可能又有一個疑問,那就是:calculate() 這個函式難道不會引起編譯器報錯嗎?
// 函式回傳值型別是Int,實際上卻拋出了例外,沒有回傳Int
// ↓ ↓
fun calculate(): Int = throw NotImplementedError()
要想搞清楚這個疑問, 需要理解Kotlin的型別系統,
小結
- Koltin運算式思維是指時刻記住 Kotlin 大部分的陳述句都是運算式,它們可以產生回傳值,利用這種思維,往往可以大大簡化代碼邏輯,
Kotlin 的型別系統
類、型別和子型別
-
類(class)是指一種資料型別,類定義定義物件的屬性和方法,可以用來創建物件實體,例如
class Person(val name: String)
,用于表示一個人的屬性和行為, -
型別(type)是指一個_變數或運算式 **的 **資料型別_,型別可以用來描述變數或運算式的特征和限制(取值范圍和可用的操作),在Kotlin中,每個變數或運算式都有一個確定的型別,例如Int、String、Boolean等,型別可以是可空的或非空的,例如
String?
或String
, -
子型別(subtype)是指一個型別的子集,即一個型別的值可以賦值給另一個型別的變數或運算式,例如
class Student(name: String, val grade: Int) : Person(name)
中,Student
是Person
的子型別,String
是String?
的子型別 ,
在 Kotlin 中,類和型別之間有一定的對應關系,但并不完全相同,一個類可以用于構造多個型別, 例如泛型類 List<T>
可以構造出 List<String>
、List<Int>
等不同的型別,一個型別也可以由多個類實作,例如介面型別 Runnable
可以由多個實作了 run()
方法的類實作,
子型別化
先看一段代碼:
非可空型別的 strNotNull:String ,可以賦值給 可空型別的strNullable:String? ; 可空型別的strNullable:String? 不可以賦值給 非可空型別的 strNotNull:String,
可以看出每一個Kotlin類都可以用于構造至少兩種型別,
根據子型別化的定義,String 是 String?的子型別,
看到這里可能有個疑問?沒有繼承關系,String 并沒有 繼承 String?,為啥String是 String? 的子型別,
其實我也有, 經常開發 Java 會有一個誤區:認為只有繼承關系的型別之間才可以有父子型別關系,
因為在Java中,類與型別大部分情況下都是“等價”的(在Java泛型出現前),事實上,“繼承”和“子型別化”是兩個完全不同的概念,子型別化的核心是一種型別的替代關系,
子型別化, 以下內容參考自維基百科
在編程語言理論中,子型別(動名詞,英語:subtyping(也有翻譯為子型別化))是一種型別多型的形式,這種形式下,子型別(名詞,英語:subtype)可以替換另一種相關的資料型別(超型別,英語:supertype),也就是說,針對超型別元素進行操作的子程式、函式等程式元素,也可以操作相應的子型別,如果 S 是 T 的子型別,這種子型別關系通常寫作 S <: T,意思是在任何需要使用 T 型別物件的_環境中,都可以安全地使用_ S 型別的物件,
由于子型別關系的存在,某個物件可能同時屬于多種型別,因此,子型別(英語:subtyping)是一種型別多型的形式,也被稱作子型別多型(英語:subtype polymorphism)或者包含多型(英語:inclusion polymorphism),
子型別與面向物件語言中(類或物件)的繼承是兩個概念,子型別反映了型別(即面向物件中的介面)之間的_關系_;而繼承反映了一類物件可以從另一類物件創造出來,是_語言特性 _的實作,因此,子型別也稱介面繼承;繼承稱作實作繼承,
子型別 - 維基百科,自由的百科全書
子型別化可表示為:
S <:T
以上S是T的子類,這意味著在需要T型別 值 的地方,S型別的 值 同樣適用,可以用 S 型別的 值 替換,
所以在前面的例子中, 雖然String與String?看起來沒有繼承關系,然而在我們需要用String?型別值的地方,顯然可以傳入一個型別為String的值,這在編譯上不會產生問題,反之卻不然, 所以String?是String的父型別,
繼承強調的是一種“實作上的復用”,而子型別化是一種型別語意的關系,與實作沒關系,對于 Java 語言,由于一般在宣告父子型別關系的同時也宣告了繼承的關系,所以造成了某種程度上的混淆,
型別系統
Kotlin 的型別還分為可空型別和不可空型別,Any 是所有非空型別的根型別;而 Any? 是所有可空型別的根型別, 我們猜測 Kotlin 的型別體系可能是這樣的:那Any 與 Any? 之間是什么關系呢?
Any 、Any?與 Java 的 Object
Java 當中的 Object 型別,對應 Kotlin 的“Any?”型別,但兩者并不完全等價,因為 Kotlin 的 Any 可以沒有 wait()、notify() 之類的方法,因此,我們只能說 Kotlin 的“Any?”與 Java 的 Object 是大致對應的,
下面是Java 代碼,它有三個方法,分別是可為空的 Object 型別、不可為空的 Object 型別,以及無注解的 Object 型別,
public class TestTypeJava {
@Nullable // 可空注解
public Object test() { return null; }
// 默認
public Object test1() { return null; }
@NotNull // 不可空注解
public Object test2() { return 1; }
}
上面的代碼通過 Convert Java File to Kotlin File
轉換成 Kotlin:
class TestTypeJava {
// 可空注解
fun test(): Any? {
return null
}
fun test1(): Any? { // 可以看出默認情況下, Java Object 對應 Kotlin Any?
return null
}
// 不可空注解
fun test2(): Any {
return 1
}
}
可以看出默認情況下,沒有注解標記可空資訊的時候, Java Object 對應 Kotlin Any?,
有些時候Java代碼包含了可空性的資訊,這些資訊使用注解來表達,當代碼中出現了這樣的資訊時,Kotlin就會使用它,因此Java中的@Nullable String被Kotlin當作String?,而@NotNull String就是String
如果沒有是否可空注解, Java型別會變成 Kotlin 中的平臺型別(后面會解釋),
了解了 Any 和 Any?的關系,可以畫出關系圖
Unit 與 Void 與 void
先看一段 Java 代碼
public class PrintHello {
public void printHelloWorld() {
System.out.println("Hello World!");
}
}
轉成 Kotlin
class PrintHello {
fun printHelloWorld():Unit { // Redundant 'Unit' return type
println("Hello World!")
}
}
Java 的 void
關鍵字在 Kotlin 里是沒有的,取而代之的是一個叫做 Unit
的東西,
Unit 和 Java 的 void
真正的區別在于,void
是真的表示什么都不回傳,而 Kotlin 的 Unit
卻是一個真實存在的型別:
public object Unit {
override fun toString() = "kotlin.Unit"
}
它是一個 object
,也就是 Kotlin 里的單例型別或者說單例物件,當一個函式的回傳值型別是 Unit
的時候,它是需要回傳一個 Unit
型別的物件的:
fun printHelloWorld():Unit {
println("Hello World!")
return Unit // return Unit 可以省略
}
只不過因為它是個 object
,所以唯一能回傳的值就是 Unit
本身,
這兩個 Unit
是不一樣的,上面的是 Unit
這個型別,下面的是 Unit
這個單例物件,它倆長得一樣但是是不同的東西,注意了,這個并不是 Kotlin 給Unit
的特權,而是 object
本來就有的語法特性,如果有需要,也可以用同樣的格式來使用別的單例物件,是不會報錯的:
包括也可以這樣寫:
val unit: Unit = Unit
也是一樣的道理,等號左邊是型別,等號右邊是物件——當然這么寫沒什么實際作用啊,單例可以直接用,
object Zhangsan
fun getZhangsan(): Zhangsan { // 單例可以直接使用
return Zhangsan
}
因此,在結構上,Unit
并沒有任何特別之處,它只是 Kotlin 的 object
,除了對于函式回傳值型別和回傳值的自動補充之外,它的特殊之處更多地在于語意和用途的角度,它是由官方規定的,用于表示「什么也不回傳」的場景的回傳值型別,但這只是它被規定的用法而已,本質上它是一個實實在在的型別,在 Kotlin 中,不存在真正沒有回傳值的函式,所有「沒有回傳值」的函式實質上的回傳值型別都是 Unit,而回傳值也都是 Unit 這個單例物件,這是 Unit 和 Java 的 void 在本質上的不同之處,
Unit 相比 void 帶來什么不同
Unit 去除了無回傳值函式的特殊性和有回傳值函式之間的本質區別,從而使得很多事情變得更加簡單,這種通用性為我們帶來了便利,
例子: 函式型別的函式引數
雖然不能說Java中的所有函式呼叫都是運算式,但是可以說Kotlin中的所有函式呼叫都是運算式,
是因為存在特例void,在Java中如果宣告的函式沒有回傳值,那么它就需要用void來修飾,如:
public void printHelloWorld() {
System.out.println("Hello World!");
}
因為 void 不是型別,所以 函式printHelloWorld()無法匹配 () -> Unit 函式型別
class VoidTest {
fun printHelloWorld1():Unit { // 作為引數時,就有函式型別 () -> Unit
println("Hello World!")
}
fun runTask(task: () -> Any) {
when (val result = task()) {
Unit -> println("result is Unit")
String -> println("result is a String: $result")
else -> println("result is an unknown type")
}
}
@Test
fun main1() {
val var1 = ::printHelloWorld1 // () -> Unit
runTask (var1) // () -> Unit
runTask { "This is string" } //:() -> String
runTask { 42 } // () -> Int
}
}
現在有了 Unit , fun printHelloWorld1():Unit 作為引數時,就有函式型別 () -> Unit ,
注意:在 Java 當中,Void 和 void 不是一回事(注意大小寫),前者是一個 Java 的類,后者是一個用于修飾方法的關鍵字,如下所示:
public final class Void {
@SuppressWarnings("unchecked")
public static final Class<Void> TYPE = (Class<Void>) Class.getPrimitiveClass("void");
private Void() {}
}
JAVA中Void類是一個不可實體化的占位符類,用來保存一個參考代表Java關鍵字void的Class物件,它的作用是在反射或泛型中表示void型別, 例如:Map介面的put方法需要兩個型別引數,如果我們只需要存盤鍵而不需要存盤值,就可以使用Void類作為型別引數
Map<String, Void> map = new HashMap<>(); map.put("key", null);,
了解了 Unit
和 Unit?
的關系后,可以畫出關系圖
Nothing
Nothing 是 Kotlin 所有型別的子型別, Noting 的概念與 Any? 恰好相反,
Nothing 也叫底型別(BottomType),
Nothing的原始碼是這樣的:
public class Nothing private constructor()
可以看到它本身雖然是 public 的,但它的建構式是 private 的,這就導致我們沒法創建它的實體;而且它不像 Unit 那樣是個 object:
public object Unit {
override fun toString() = "kotlin.Unit"
}
而是個普通的 class;并且在原始碼里 Kotlin 也沒有幫我們創建它的實體, 這些條件加起來,結果就是:Nothing 這個類既沒有、也不會有任何的實體物件, 基于這樣的前提,當我們寫出這個函式宣告的時候:
fun nothing(): Nothing {
}
我們可能無法找到一個合適的值來回傳,但是在撰寫代碼時,我們必須回傳一個值,這種情況下,我們遇到了一個悖論,即必須回傳一個值,但卻永遠找不到合適的回傳值
Nothing的作用: 作為函式 永遠不會回傳結果
的提示
fun nothing() : Nothing {
throw RuntimeException("Nothing!")
}
根據Nothing的特性, Nothing 專門用于拋例外,
public class NotImplementedError(message: String = "An operation is not implemented.") : Error(message)
@kotlin.internal.InlineOnly
public inline fun TODO(): Nothing = throw NotImplementedError()
從上面這段代碼可以看出,Kotin 原始碼中 throw 運算式的回傳值型別是 Nothing,
throw 這個運算式的回傳值是 Nothing 型別,而既然 Nothing 是所有型別的子型別,那么它當然是可以賦值給任意其他型別的, 所以運算式思維中的問題就可以解答了
// 函式回傳值型別是Int,實際上卻拋出了例外,沒有回傳Int
// ↓ ↓
fun calculate(): Int = throw NotImplementedError()
作用二
Nothing 類的建構式是私有的,因此我們無法構造出它的實體,當 Nothing 型別作為函式引數時,一個有趣的現象就出現了:
// 這是一個無法呼叫的函式,因為找不到合適的引數
fun show(msg: Nothing) {}
show(null) // 報錯
show(throw Exception()) // 雖然不報錯,但方法仍然不會呼叫
在這里,我們定義了一個 show 函式,它的引數型別是 Nothing,由于 Nothing 的建構式是私有的,我們將無法呼叫 show 函式,除非我們拋出例外,但這沒有意義, 這個概念在泛型星投影的時候是有應用的,具體后面會解釋,
作用三
而除此之外,Nothing 還有助于編譯器進行代碼流程的推斷,比如說,當一個運算式的回傳值是 Nothing 的時候,就往往意味著它后面的陳述句不再有機會被執行,如下圖所示:
了解了 Nothing 和 Nothing?的關系后,可以畫出關系圖
平臺型別
image.png
平臺型別在Kotlin中表示為type!(如String!,Int!, CustomClass!), Kotlin平臺型別本質上就是Kotlin不知道可空性資訊的型別,即可以當作可空型別,也可以當作非空型別,平臺型別只能來自Java,因為Java中所有的參考都可能為null,而Kotlin中對null有嚴格的檢查和限制, 但是在Kotlin中是禁止宣告平臺型別的變數的,
image.png
具體的代碼示例如下:
// Java 代碼
public class Person {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
// Kotlin 代碼
fun main() {
val person = Person() //
val name = person.name // name 是 String! 型別
println(name.length) // 可能拋出空指標例外
person.name = null // 允許賦值為 null
}
在這個例子中, name 是平臺型別,
因為它們來自于 Java 代碼,Kotlin 編譯器不會檢查它們是否為 null,所以需要程式員自己負責,如果要避免空指標例外,可以使用安全呼叫運算子(?.)或非空斷言運算子(!!)來處理平臺型別,
println(name?.length) // 安全呼叫,如果 name 為 null 則回傳 null
println(name!!.length) // 非空斷言,如果 name 為 null 則拋出例外
平臺型別是指 Kotlin 和 Java 的互操作性問題, 在混合專案中要多加注意,
小結
-
Any 是所有非空型別的根型別,而 Any? 才是所有型別的根型別,
-
Unit 與 Java 的 void 型別相似,代表一個函式不需要回傳值;而 Unit? 這個型別則沒有太多實際的意義,
-
當 Nothing 作為函式回傳值時,意味著這個函式永遠不會回傳結果,而且還會截斷程式的后續流程,Kotlin 編譯器也會根據這一點進行流程分析,
-
當 Nothing 作為函式引數時,就意味著這個函式永遠無法被正常呼叫,這在泛型星投影的時候是有一定應用的,
-
Nothing 可以看作是 Nothing? 的子型別,因此,Nothing 可以看作是 Kotlin 所有型別的底型別,
-
正是因為 Kotlin 在型別系統中加入了 Unit、Nothing 這兩個型別,才讓大部分無法產生值的陳述句搖身一變,成為了運算式,這也是“Kotlin 大部分的陳述句都是運算式”的根本原因,
泛型:讓型別更加安全
Kotlin 的泛型與 Java 一樣,都是一種語法糖,即只在源代碼中有泛型定義,到了class級別就被擦除了, 泛型(Generics)其實就是把型別引數化,真正的名字叫做型別引數,它的引入給強型別編程語言加入了更強的靈活性,
泛型的優點
-
型別安全:泛型可以在編譯時檢查型別,從而避免了在運行時出現型別不匹配的錯誤,這可以提高程式的可靠性和穩定性,
-
代碼重用:泛型可以使代碼更加通用和靈活,從而可以減少代碼的重復和冗余,例如,我們可以撰寫一個通用的排序演算法,可以用于任何實作了 Comparable 介面的型別,
在 Java 中,我們常見的泛型有:泛型類、泛型介面、泛型方法和泛型屬性,Kotlin 泛型系統繼承了 Java 泛型系統,同時添加了一些強化的地方,
泛型介面/類(泛型型別)
定義泛型型別,是在型別名之后、主建構式之前用尖括號括起的大寫字母型別引數指定:
宣告泛型介面
Java:
//泛型介面
interface Drinks<T> {
T taste();
void price(T t);
}
Kotlin:
//泛型介面
interface Drinks<T> {
fun taste(): T
fun price(t: T)
}
宣告泛型類
Java
abstract class Color<T> {
T t;
abstract void printColor();
}
class Blue {
String color = "blue";
}
class BlueColor extends Color<Blue> {
public BlueColor(Blue1 t) {
this.t = t;
}
@Override
public void printColor() {
System.out.println("color:" + t.color);
}
}
Kotlin
abstract class Color<T>(var t: T/*泛型欄位*/) {
abstract fun printColor()
}
class Blue {
val color = "blue"
}
class BlueColor(t: Blue) : Color<Blue>(t) {
override fun printColor() {
println("color:${t.color}")
}
}
泛型欄位
定義泛型型別欄位,可以完整地寫明型別引數,如果編譯器可以自動推定型別引數,也可以省略型別引數:
abstract class Color<T>(var t: T/*泛型欄位*/) {
abstract fun printColor()
}
宣告泛型方法
Kotlin 泛型方法的宣告與 Java 相同,型別引數要放在方法名的前面:
Java
public static <T> T fromJson(String json, Class<T> tClass) {
T t = null;
try {
t = tClass.newInstance();
} catch (Exception e) {
e.printStackTrace();
}
return t;
}
Kotlin
fun <T> fromJson(json: String, tClass: Class<T>): T? {
/*獲取T的實體*/
val t: T? = tClass.newInstance()
return t
}
泛型約束
Java 中可以通過有界型別引數來限制引數型別的邊界,Kotlin中泛型約束也可以限制引數型別的上界:
Java
public static <T extends Comparable<T>> T maxOf(T a, T b) {
if (a.compareTo(b) > 0) return a;
else return b;
}
Kotlin
fun <T : Comparable<T>> maxOf(a: T, b: T): T {
return if (a > b) a else b
}
image.png
where關鍵字: 多個上界用 where
Java 中多約束: &
public static <T extends CharSequence & Comparable<T>> List<T> test(List<T> list, T threshold) {
return list.stream().filter(it -> it.compareTo(threshold) > 0).collect(Collectors.toList());
}
Kotin 中多約束:where
//多個上界的情況
fun <T> test(list: List<T>, threshold: T): List<T>
where T : CharSequence,
T : Comparable<T> {
return list.filter { it > threshold }.map { it }
}
所傳遞的型別T必須同時滿足 where 子句的所有條件,在上述示例中,型別 T 必須既實作了 CharSequence 也實作了 Comparable,
泛型形參&泛型實參
泛型類:
泛型函式:
泛型的型變
不變
先看一段 Java 代碼,我們知道在Java中 ,List無法賦值給List
public class JavaGeneryc {
public static void main(String[] args) {
List<Apple> apples = new ArrayList<>();
apples.add(new Apple());
List<Fruit> fruits = apples; // 編譯錯誤
for (Fruit fruit : fruits) {
System.out.println(fruit);
}
}
}
class Fruit {
// 父類
}
class Apple extends Fruit {
// 子類
}
image.png
但是到了Kotlin這里我們發現了一個奇怪的現象
fun main2(args: Array<String>) {
val stringList:List<String> = ArrayList<String>()
val anyList:List<Any> = stringList//編譯成功
}
image.png
在Kotlin中竟然能將List賦值給List,不是說好的Kotlin和Java的泛型原理是一樣的嗎?怎么到了Kotlin中就變了?其實我們前面說的都沒錯,關鍵在于這兩個List并不是同一種型別,我們分別來看一下兩種List的定義:
雖然都叫List,也同樣支持泛型,但是Kotlin的List定義的泛型引數前面多了一個 out關鍵詞(加上out 發生協變 ),這個關鍵詞就對這個List的特性起到了很大的作用, 普通方式定義的泛型是不變的,簡單來說就是不管型別A和型別B是什么關系,Generic與Generic(其中Generic代表泛型類)都沒有任何關系,比如,在Java中String是Oject的子型別,但List并不是List的子型別,在Kotlin中泛型的原理也是一樣的,Kotin 使用 out 才發生了變化,
**
out 位置與 in 位置
函式引數的型別叫作in位置,而函式回傳型別叫作out位置
協變 :保留子型別化關系
如果在定義的泛型類和泛型方法的泛型引數前面加上out關鍵詞,說明這個泛型類及泛型方法是協變,簡單來說型別A是型別B的子型別,那么Generic也是Generic的子型別,
**
image.png
協變點 (out 位置)
函式回傳值型別為泛型引數,
協變的特征
只能消費,只能取
-
子型別化會被保留(Producer是Producer的子型別)
-
T只能用在out位置
image.png
interface Book
interface EduBook : Book
class BookStore<out T : Book> {
fun getBook(): T {
TODO()
}
}
fun covariant(){
// 教材書店
val eduBookStore: BookStore<EduBook> = BookStore<EduBook>()
// 書店
val bookStore: BookStore<Book> = eduBookStore // 協變,教輔書店是書店的子型別
val book: Book = bookStore.getBook()
val eduBook : EduBook = eduBookStore.getBook()
}
image.png
協變小結
?子型別 Derived 兼容父型別 Base ?生產者 Producer<Derived>兼容 Producer
逆變: 反轉子型別化關系
如果在定義的泛型類和泛型方法的泛型引數前面加上in關鍵詞,說明這個泛型類及泛型方法是逆變,簡單來說型別A是型別B的子型別,那么Generic是Generic****的子型別,型別父子關系反轉,
**
逆變點 (in 位置)
函式引數型別為泛型引數,
逆變的特征
只能生產,只能放入
-
子型別化會被反轉(Consumer是 Consumer的子型別)
-
T只能用在in位置
image.png
垃圾不能扔到干垃圾桶,但是可以扔到垃圾桶, 干垃圾可以扔到垃圾桶,也可以扔到垃圾桶, 由此可以看出垃圾桶可以替代干垃圾桶, 所以干垃圾桶是父型別,
open class Waste
// 干垃圾
class DryWaste : Waste()
// 垃圾桶
class Dustbin<in T : Waste> {
fun put(t: T) {
TODO()
}
}
fun contravariant(){
val dustbin: Dustbin<Waste> = Dustbin<Waste>()
val dryWasteDustbin: Dustbin<DryWaste> = dustbin
val waste = Waste()
val dryWaste = DryWaste()
dustbin.put(waste)
dustbin.put(dryWaste)
// dryWasteDustbin.put(waste)
dryWasteDustbin.put(dryWaste)
}
宣告為 in ,在 out 位置使用,是會報錯的,
image.png
逆變小結
-
子型別 Derived 兼容父型別 Base
-
消費者 Consumer兼容 Consumer< Derived>
-
記憶小技巧: in 表示逆變, in 倒序過來是 ni(逆),
型變小結
協變 | 逆變 | 不變型 |
---|---|---|
Producer | Consumer | MutableList: |
類的子型別化保留了:Producers是 Producer<Animal>的子型別 | 子型別化反轉了:Consumer是 Consumer的子型別 | 沒有子型別化 |
T只能在out 位置 | T只能在 in 位置 | T可以在任何位置 |
泛型中的out與in與 Java 上下界通配符關系
在Kotlin中out代表協變,in代表逆變,為了加深理解我們可以將Kotlin的協變看成Java的上界通配符,將逆變看成Java的下界通配符:
//Kotlin使用處協變
fun sumOfList(list: List<out Number>)
//Java上界通配符
void sumOfList(List<? extends Number> list)
//Kotlin使用處逆變
fun addNumbers(list: List<in Int>)
//Java下界通配符
void addNumbers(List<? super Integer> list)
小結
Java 泛型 | Java 中代碼示例 | Kotlin 中代碼示例 | Kotlin 泛型 |
---|---|---|---|
泛型型別 | class Box | class Box | 泛型型別 |
泛型方法 | T fromJson(String json, ClasstClass) | funfromJson(json: String, tClass: Class): T? | 泛型函式 |
有界型別引數 | class Box<T extends Comparable | class Box<T : Comparable> | 泛型約束 |
上界通配符 | void sumOfList(List<? extends Number> list) | fun sumOfList(list: List) | 使用處協變 |
下界通配符 | void addNumbers(List<? super Integer> list) | fun addNumbers(list: List) | 使用處逆變 |
總的來說,Kotlin 泛型更加簡潔安全,但是和 Java 一樣都是有型別擦除的,都屬于編譯時泛型,
下期分享:
星投影
注解 @UnsafeVariance
行內特化(行內強化) reified
系列
From Java To Kotlin:空安全、擴展、函式、Lambda很詳細,這次終于懂了
From Java To Kotlin 2:Kotlin 型別系統與泛型
作者:Seachal
出處:http://www.cnblogs.com/ZhangSeachal
如果,您認為閱讀這篇博客讓您有些識訓,不妨點擊一下左下角的【好文要頂】與【收藏該文】
如果,您希望更容易地發現我的新博客,不妨點擊一下左下角的【關注我】
如果,您對我的博客內容感興趣,請繼續關注我的后續博客,我是【Seachal】
我的GitHub
我的CSDN
我的簡書
本博文為學習、筆記之用,以筆記記錄作者學習的知識與學習后的思考或感悟,學習程序可能參考各種資料,如覺文中表述過分參考,請務必告知,以便迅速處理,如有錯漏,不吝賜教!
轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/554416.html
標籤:其他
下一篇:返回列表