From Java To Kotlin 2:Kotlin 類型系統與泛型

来源:https://www.cnblogs.com/Seachal/archive/2023/06/06/17459617.html
-Advertisement-
Play Games

上期主要分享了 From Java To Kotlin 1 :空安全、擴展、函數、Lambda。 這是 From Java to Kotlin 第二期。 From Java to Kotlin 關鍵在於 **思維的轉變**。 ...


上期主要分享了 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) 中,StudentPerson 的子類型,StringString?的子類型  。

在 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);。

瞭解了 UnitUnit?的關係後,可以畫出關係圖圖片

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)其實就是把類型參數化,真正的名字叫做類型參數,它的引入給強類型編程語言加入了更強的靈活性。

泛型的優點

  1. 類型安全:泛型可以在編譯時檢查類型,從而避免了在運行時出現類型不匹配的錯誤。這可以提高程式的可靠性和穩定性。

  2. 代碼重用:泛型可以使代碼更加通用和靈活,從而可以減少代碼的重覆和冗餘。例如,我們可以編寫一個通用的排序演算法,可以用於任何實現了 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 我的簡書

本博文為學習、筆記之用,以筆記記錄作者學習的知識與學習後的思考或感悟。學習過程可能參考各種資料,如覺文中表述過分引用,請務必告知,以便迅速處理。如有錯漏,不吝賜教!


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 目錄 一、正則表達式 二、元字元 三、次數符號 四、位置錨定 五、實驗 一、正則表達式 通配符功能是用來處理文件名,而正則表達式是處理文本內容中字元。 分類: 1. 基本正則表達式 2.擴展正則表達式 二、元字元 元字元: . 匹配任意單個字元,可以是一個漢字 () 使用轉義符,只表示\(\) [] ...
  • ## 概述 Hive的執行計劃描述了一個hiveSQL語句的具體執行步驟,通過執行計劃解讀可以瞭解hiveSQL語句被解析器轉換為相應程式語言的執行邏輯。通過執行邏輯可以知曉HiveSQL運行流程,進而對流程進行優化,實現更優的數據查詢處理。 同樣,通過執行計劃,還可以瞭解到哪些不一樣的SQL邏輯其 ...
  • 今天這個資料庫來源於《漢藏英常用新詞語詞典》編纂小組編纂、四川民族出版社和四川出版集團出版的《漢藏英常用新詞語詞典》及其增補本。具體看截圖,截圖包含所有欄位: 目錄彙總:藏漢大辭典(25228)、藏漢英信息技術詞典(11763)、漢藏對照詞典(82530)、漢藏英常用新詞語詞典(9649)。 截圖下 ...
  • 那些眾多的旅游網站里,什麼景點、什麼攻略、什麼路線、什麼酒店啊幾乎都是一模一樣,這得益以現在採集工具的“魔高一丈”,但是似乎很少見到各個旅游景點所在地區的特產介紹。而今天在互聯網上提取出了一份: 這個ACCESS資料庫包含省份、城市、地方特產三個表,其中城市表可關聯到省份表、地方特產表可以關聯到城市 ...
  • 我很喜歡這種圖譜、名冊、字典類的資料庫,像這種資料庫還有《史前古生物資料圖譜ACCESS資料庫》、《中國魚類資料圖譜大全ACCESS資料庫》、《植物結構部件資料圖譜ACCESS資料庫》、《全球家畜資料圖譜大全ACCESS資料庫》等。 幾乎每一個鳥類都會對應一張圖,只有28條記錄圖片丟失;包含目表、科 ...
  • 之前發過幾個關於趣味心理測試的數據,而今天的這個內容與他們是一類的只不過結構方面很不同,前者是結構完美的測試題,今天這個只是個文章型的數據、文章型的測試,看截圖: 比如下麵這個就是其中一條記錄的詳細內容,標題是“你現在最重視誰?”,以下是內容: 每個人都或多或少有掉東西的經驗。假設有一天,你騎機車經 ...
  • 1. 背景 3月份針對線上重點H5項目秒開進行治理,本文將逐步介紹如何通過H5頁面的優化手段來提高 1.5 秒開率。 2. 為什麼要優化 從用戶角度看,優化能夠讓頁面載入得更快、對用戶操作響應更及時,用戶體驗更良好,提升用戶體驗和降低用戶流失率非常重要。其中 Global Web Performan ...
  • 我們團隊接到了食品頻道的一個互動項目的開發需求,希望通過 3D 場景的展示和互動方式,作為對未來購物的一種嘗試與探索,滿足用戶對未來美好新奇的一個需求。將購物場景化、娛樂化,給用戶帶來美好的購物感受。 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...