主頁 > 移動端開發 > From Java To Kotlin 2:Kotlin 型別系統與泛型

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

2023-06-06 13:29:25 移動端開發

上期主要分享了 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 我的簡書

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

轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/554416.html

標籤:其他

上一篇:126個專業心理測驗系統ACCESS資料庫

下一篇:返回列表

標籤雲
其他(160433) Python(38206) JavaScript(25478) Java(18198) C(15237) 區塊鏈(8270) C#(7972) AI(7469) 爪哇(7425) MySQL(7234) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5873) 数组(5741) R(5409) Linux(5346) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4585) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2434) ASP.NET(2403) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) .NET技术(1981) 功能(1967) HtmlCss(1952) Web開發(1951) C++(1929) python-3.x(1918) 弹簧靴(1913) xml(1889) PostgreSQL(1879) .NETCore(1863) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【從零開始擼一個App】Dagger2

    Dagger2是一個IOC框架,一般用于Android平臺,第一次接觸的朋友,一定會被搞得暈頭轉向。它延續了Java平臺Spring框架代碼碎片化,注解滿天飛的傳統。嘗試將各處代碼片段串聯起來,理清思緒,真不是件容易的事。更不用說還有各版本細微的差別。 與Spring不同的是,Spring是通過反射 ......

    uj5u.com 2020-09-10 06:57:59 more
  • Flutter Weekly Issue 66

    新聞 Flutter 季度調研結果分享 教程 Flutter+FaaS一體化任務編排的思考與設計 詳解Dart中如何通過注解生成代碼 GitHub 用對了嗎?Flutter 團隊分享如何管理大型開源專案 插件 flutter-bubble-tab-indicator A Flutter librar ......

    uj5u.com 2020-09-10 06:58:52 more
  • Proguard 常用規則

    介紹 Proguard 入口,如何查看輸出,如何使用 keep 設定入口以及使用實體,如何配置壓縮,混淆,校驗等規則。

    ......

    uj5u.com 2020-09-10 06:59:00 more
  • Android 開發技術周報 Issue#292

    新聞 Android即將獲得類AirDrop功能:可向附近設備快速分享檔案 谷歌為安卓檔案管理應用引入可安全隱藏資料的Safe Folder功能 Android TV新主界面將顯示電影、電視節目和應用推薦內容 泄露的Android檔案暗示了傳說中的谷歌Pixel 5a與折疊屏新機 谷歌發布Andro ......

    uj5u.com 2020-09-10 07:00:37 more
  • AutoFitTextureView Error inflating class

    報錯: Binary XML file line #0: Binary XML file line #0: Error inflating class xxx.AutoFitTextureView 解決: <com.example.testy2.AutoFitTextureView android: ......

    uj5u.com 2020-09-10 07:00:41 more
  • 根據Uri,Cursor沒有獲取到對應的屬性

    Android: 背景:呼叫攝像頭,拍攝視頻,指定保存的地址,但是回傳的Cursor檔案,只有名稱和大小的屬性,沒有其他諸如時長,連ID屬性都沒有 使用 cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DURATIO ......

    uj5u.com 2020-09-10 07:00:44 more
  • Android連載29-持久化技術

    一、持久化技術 我們平時所使用的APP產生的資料,在記憶體中都是瞬時的,會隨著斷電、關機等丟失資料,因此android系統采用了持久化技術,用于存盤這些“瞬時”資料 持久化技術包括:檔案存盤、SharedPreference存盤以及資料庫存盤,還有更復雜的SD卡記憶體儲。 二、檔案存盤 最基本存盤方式, ......

    uj5u.com 2020-09-10 07:00:47 more
  • Android Camera2Video整合到自己專案里

    背景: Android專案里呼叫攝像頭拍攝視頻,原本使用的 MediaStore.ACTION_VIDEO_CAPTURE, 后來因專案需要,改成了camera2 1.Camera2Video 官方demo有點問題,下載后,不能直接整合到專案 問題1.多次拍攝視頻崩潰 問題2.雙擊record按鈕, ......

    uj5u.com 2020-09-10 07:00:50 more
  • Android 開發技術周報 Issue#293

    新聞 谷歌為Android TV開發者提供多種新功能 Android 11將自動填表功能整合到鍵盤輸入建議中 谷歌宣布Android Auto即將支持更多的導航和數字停車應用 谷歌Pixel 5只有XL版本 搭載驍龍765G且將比Pixel 4更便宜 [圖]Wear OS將迎來重磅更新:應用啟動時間 ......

    uj5u.com 2020-09-10 07:01:38 more
  • 海豚星空掃碼投屏 Android 接收端 SDK 集成 六步驟

    掃碼投屏,開放網路,獨占設備,不需要額外下載軟體,微信掃碼,發現設備。支持標準DLNA協議,支持倍速播放。視頻,音頻,圖片投屏。好點意思。還支持自定義基于 DLNA 擴展的操作動作。好像要收費,沒體驗。 這里簡單記錄一下集成程序。 一 跟目錄的build.gradle添加私有mevan倉庫 mave ......

    uj5u.com 2020-09-10 07:01:43 more
最新发布
  • From Java To Kotlin 2:Kotlin 型別系統與泛型

    上期主要分享了 From Java To Kotlin 1 :空安全、擴展、函式、Lambda。

    這是 From Java to Kotlin 第二期。
    From Java to Kotlin 關鍵在于 **思維的轉變**。 ......

    uj5u.com 2023-06-06 13:29:25 more
  • 126個專業心理測驗系統ACCESS資料庫

    今天這份資料庫是從一個心理測驗軟體破解出來的一系列的心理自量表,人格自量表以及精神病自量表等量化心理測驗表,適用于醫院、學校、職場等機構進行心里咨詢,并且可以進行診斷測驗。 【續后:原150個測驗專案經過第二次整理檢查去除無效資料僅有126個測驗專案,實際記錄數會和上面截圖顯示的記錄數有差別】 該數 ......

    uj5u.com 2023-06-06 13:27:42 more
  • 大型 3D 互動開發和優化實踐

    我們團隊接到了食品頻道的一個互動專案的開發需求,希望通過 3D 場景的展示和互動方式,作為對未來購物的一種嘗試與探索,滿足用戶對未來美好新奇的一個需求。將購物場景化、娛樂化,給用戶帶來美好的購物感受。 ......

    uj5u.com 2023-06-06 13:26:14 more
  • From Java To Kotlin 2:Kotlin 型別系統與泛型

    上期主要分享了 From Java To Kotlin 1 :空安全、擴展、函式、Lambda。

    這是 From Java to Kotlin 第二期。
    From Java to Kotlin 關鍵在于 **思維的轉變**。 ......

    uj5u.com 2023-06-06 13:25:56 more
  • 126個專業心理測驗系統ACCESS資料庫

    今天這份資料庫是從一個心理測驗軟體破解出來的一系列的心理自量表,人格自量表以及精神病自量表等量化心理測驗表,適用于醫院、學校、職場等機構進行心里咨詢,并且可以進行診斷測驗。 【續后:原150個測驗專案經過第二次整理檢查去除無效資料僅有126個測驗專案,實際記錄數會和上面截圖顯示的記錄數有差別】 該數 ......

    uj5u.com 2023-06-06 13:12:29 more
  • 預約直播|揭秘鴻蒙全新流量陣地,元服務帶來的體驗變革

    **【導讀】** 在PC 互聯網到移動互聯網的演程序序,隨著人們對互動和資訊獲取的智能化要求越來越高,移動終端上的應用生態發展到今天也面臨著變革。傳統厚重的App,功能齊全,但開發成本高、周期長,且存在搜索、安裝、卸載等一系列需要用戶主動關注的顯性操作,這些顯性操作給用戶帶來了實質性的使用成本。輕量 ......

    uj5u.com 2023-06-03 08:48:34 more
  • 預約直播|揭秘鴻蒙全新流量陣地,元服務帶來的體驗變革

    **【導讀】** 在PC 互聯網到移動互聯網的演程序序,隨著人們對互動和資訊獲取的智能化要求越來越高,移動終端上的應用生態發展到今天也面臨著變革。傳統厚重的App,功能齊全,但開發成本高、周期長,且存在搜索、安裝、卸載等一系列需要用戶主動關注的顯性操作,這些顯性操作給用戶帶來了實質性的使用成本。輕量 ......

    uj5u.com 2023-06-03 08:47:51 more
  • Android strings.xml按照key修改

    ## strings.xml匹配替換 將兩個Android專案中的多語言字串檔案(strings.xml)進行比較,如果其中一個專案中包含另一個專案沒有的字符,則合并到單一的輸出檔案,并以 key 在原始 XML 檔案中更新 value 值。如果key匹配不準確則忽略它。 具體來說: 1. 引入 ......

    uj5u.com 2023-06-02 12:09:15 more
  • Android strings.xml按照key修改

    ## strings.xml匹配替換 將兩個Android專案中的多語言字串檔案(strings.xml)進行比較,如果其中一個專案中包含另一個專案沒有的字符,則合并到單一的輸出檔案,并以 key 在原始 XML 檔案中更新 value 值。如果key匹配不準確則忽略它。 具體來說: 1. 引入 ......

    uj5u.com 2023-06-02 12:08:22 more
  • 談談ChatGPT是否可以替代人

    起初我以為我是搬磚的,最近發現其實只是一塊磚,哪里需要哪里搬。 ![](https://img2023.cnblogs.com/other/3070683/202306/3070683-20230601071013776-739239962.gif) 這兩天臨時被抽去支援跨平臺相關軟體開發,幫忙畫幾 ......

    uj5u.com 2023-06-01 10:22:09 more