Kotlin 知識梳理(7) Kotlin 的型別系統

澤毛發表於2017-12-21

一、本文概要

本文是對<<Kotlin in Action>>的學習筆記,如果需要執行相應的程式碼可以訪問線上環境 try.kotlinlang.org,這部分的思維導圖為:

Kotlin 知識梳理(7)   Kotlin 的型別系統

二、基本資料型別和其它基本型別

2.1 基本型別:Int、Boolean 及其它

Java把基本資料型別和引用型別做了區分:

  • 基本資料型別,例如int的變數直接儲存了它的值,我們不能對這些值呼叫方法,或者把它們放到集合中。
  • 引用型別的變數儲存的是指向包含該物件的記憶體地址的引用。

Kotlin不區分基本資料型別和引用型別,它使用的永遠是一個型別(例如Int),此外,你還能對一個數字型別的值呼叫方法。

在執行時,數字型別會盡可能地使用最高效的方式來表示,大多數情況下,對於變數、屬性、引數和返回型別,KotlinInt型別會被編譯成Java基本資料型別int。唯一不可行的例外是泛型類,例如集合,用作泛型型別引數的基本資料型別會被編譯成物件的Java包型別。

對應到Java基本資料型別的型別完整列表如下:

  • 整數型別:ByteShortIntLong
  • 浮點數型別:FloatDouble
  • 字元型別:Char
  • 布林型別:Boolean

Int這樣的Kotlin型別在底層可以輕易地編譯成對應的Java基本資料型別。而在Kotlin中使用Java宣告時,Java基本資料型別會變成非空型別,因為它們不能持有null值。

2.2 可空的基本資料型別:Int?、Boolean? 及其它

Kotlin中的可空型別不能用Java的基本資料型別表示,因為null只能被儲存在Java的引用型別的變數中。任何時候,只要使用了基本資料型別的可空版本,它就會被編譯成對應的包裝型別,並且不能比較兩個可空基本資料型別的大小,因為它們之中任何一個都可能為null

除此之外,泛型類是包裝型別應用的另一種情況,如果你 用基本資料型別作為泛型類的型別引數,那麼 Kotlin 會使用該型別的包裝形式,例如下面這段程式碼,就會建立一個Integer包裝類的列表,儘管你從來沒有指定過可空型別或者用過null值:

val listOfInts = listOf(1, 2, 3)
複製程式碼

這是由Java虛擬機器實現泛型的方式決定的,JVM不支援用基本資料型別作為型別引數,所以泛型類必須始終使用型別的包裝表示。

2.3 數字轉換

KotlinJava之間一條重要的區別就是處理數字轉換的方式,Kotlin不會自動地把數字從一種型別轉換成另一種,即便是轉換成範圍更大的型別,我們必須 顯示地轉換,對每一種基本資料型別都定義有轉換函式:toByte()toShort()toChar()等,這些函式支援雙向轉換:

Kotlin 知識梳理(7)   Kotlin 的型別系統
在比較裝箱值的時候,比較兩個裝箱值的equals不僅會檢查它們儲存的值,還要比較裝箱型別,也就是說new Integer(42).equals(new Long(42))會返回false

基本資料型別字面值

Kotlin除了支援簡單的十進位制數字之外,還支援下面這些在程式碼中書籤數字字面值的方式:

  • 使用字尾L表示Long123L
  • 使用標準浮點數表示Double0.121.2e101.2e-10
  • 使用字尾F表示Float123.4f.456F1e3f
  • 使用字首0x或者0X表示十六進位制:0xbcdL
  • 使用字首0b或者0B表示二進位制字面值:0b0001

當你使用數字字面值去初始化一個型別已知的變數時,又或是把字面值作為實參傳給函式時,必要的轉換會自動地發生

Kotlin 知識梳理(7)   Kotlin 的型別系統
此外,算術運算子也被過載了,它們可以接收所有適當的數字型別。

2.4 根型別:Any 和 Any?

Any型別是Kotlin所有非空型別的超型別,包括像Int這樣的基本資料型別,和Java一樣,把基本資料型別的值賦給Any型別的變數會自動裝箱。

Kotlin中,如果你需要可以持有任何可能值的變數,包括null在內,必須使用Any?型別。

在底層,Any型別對應java.lang.ObjectKotlinJava方法引數和返回型別中用到的Object型別看作Any,當Kotlin函式函式中使用Any時,它會被編譯成Java位元組碼中的Object

所有的Kotlin類都包含下面三個方法:toStringequalshashCode,這些方法都繼承自AnyAny不能使用其它Object的方法(例如waitnotify),但是可以通過手動把值轉換成java.lang.Object來呼叫這些方法。

2.5 Unit 型別:Kotlin 的 void

Kotlin中的Unit型別完成了Java中的void一樣的功能,當函式沒有有意思的結果要返回時,它可以用作函式的返回型別:

fun f() : Unit { .. }
複製程式碼

Unit是一個完備的型別,可以作為型別引數,而void卻不行。只存在一個值是Unit型別,這個值也叫做Unit,並且(在函式中)會被隱式地返回,當你在重寫返回泛型引數的函式時這非常有用,只需要讓方法返回Unit型別的值:

Kotlin 知識梳理(7)   Kotlin 的型別系統
執行結果為:
Kotlin 知識梳理(7)   Kotlin 的型別系統

2.6 這個函式永不返回:Nothing

對於某些Kotlin函式來說,“返回型別”的概念沒有任何意義,因為它們從來不會成功地結束,Kotlin使用一種特殊的返回型別Nothing來表示:

Kotlin 知識梳理(7)   Kotlin 的型別系統
執行結果為:
Kotlin 知識梳理(7)   Kotlin 的型別系統
Nothing型別沒有任何值,只有被當作函式返回值使用,或者被當作泛型函式返回值的型別引數使用才會有意義,在其它情況下,宣告一個不能儲存任何值的變數沒有任何意義。

返回Nothing的函式可以放在Elvis運算子的右邊來做先決條件檢查:

Kotlin 知識梳理(7)   Kotlin 的型別系統
執行結果為:
Kotlin 知識梳理(7)   Kotlin 的型別系統

三、集合和陣列

3.1 可空性和集合

Kotlin 知識梳理(6) - Kotlin 的可空性 中,我們討論了可空型別的概念,但僅僅簡略地談到型別引數的可空性,其實集合也可以持有null元素,和變數可以持有null一樣,型別在被當作型別引數時也可以用同樣的方式來標記。

下面我們建立一個包含可空值的集合,之後遍歷該集合,列印出有效的數字之和以及為null的集合元素個數:

Kotlin 知識梳理(7)   Kotlin 的型別系統
執行結果為:
Kotlin 知識梳理(7)   Kotlin 的型別系統

3.2 只讀集合與可變集合

Kotlin的集合設計與Java不同的另一項重要特質是:它把訪問集合資料的介面和修改集合資料的結構分開了:

  • kotlin.collections.Collection:使用這個介面,可以遍歷集合中的元素、獲取集合大小、判斷集合中是否包含某個元素,執行其他從該集合中讀取資料的操作。
  • kotlin.collections.MutableCollection:修改集合中的資料。

一般的原則是:在程式碼的任何地方都應該使用只讀介面,只在程式碼需要修改集合的地方使用可變介面的變體

下面的例子演示瞭如何使用只讀集合和可變集合:

Kotlin 知識梳理(7)   Kotlin 的型別系統
執行結果為:
Kotlin 知識梳理(7)   Kotlin 的型別系統

3.3 Kotlin 集合和 Java

每一個Kotlin介面都是其對應Java集合介面的一個例項,在KotlinJava之間轉移並不需要轉換;不需要包裝器也不需要拷貝資料。

每一種Java集合介面在Kotlin中都有兩種表示:一種是隻讀的,另一種是可變的。在下圖當中,可以看出Kotlin集合介面的層級結構,JavaArrayListHashSet都繼承了Kotlin可變介面。

Kotlin 知識梳理(7)   Kotlin 的型別系統
Kotlin中只讀介面和可變介面的基本結構與java.util中的Java集合介面的結構是平行的。可變介面直接對應java.util包中的介面,而它們的只讀版本缺少了所有產生改變的方法。

上圖中包含了Java類中的ArrayListHashSet,在Kotlin看來,它們分別繼承自MutableListMutableSet介面,這樣既得到了相容性,也得到了可變介面和只讀介面之間清晰的分離。

除了集合之外,KotlinMap類也被表示成了兩種不同的版本:MapMutableMap。我們之前見到的listOf/setOf/mapOf所返回的都是隻讀版本。

當你有一個使用java.util.Collection做形參的Java方法,可以把任意CollectionMutableCollection的值作為實參傳遞給這個形參。Java並不會區分只讀集合和可變集合,也就是說即使Kotlin中把集合宣告成只讀的,Java程式碼也可以修改這個集合,例如下面的程式碼,雖然我們將printInUppercase接收的list引數宣告為只讀的,但是仍然可以通過Java程式碼修改它。

//CollectionUtils.java
public class CollectionUtils {
    public static List<String> uppercaseAll(List<String> items) {
        for (int i = 0; i < items.size(); i++) {
            items.set(i, items.get(i).toUpperCase());
        }
        return items;
    }
}

//collections.kt
fun printInUppercase(list : List<String>) {
    println(CollectionUtils.uppercaseAll(list));
    println(list.first())
}
複製程式碼

3.4 作為平臺型別的集合

前面我們介紹過,Kotlin把那些定義在Java程式碼中的型別看成 平臺型別Kotlin沒有任何關於平臺型別的可空性資訊,所以編譯器允許Kotlin程式碼將其視為可空或者非空,同樣,Java中宣告的集合型別的變數也被視為平臺型別。

當我們需要重寫或者實現簽名中有集合型別的Java方法時,這些差異才變得重要,我們需要決定使用哪一種Kotlin型別來表示這個Java型別,它們會反映在產生的Kotlin引數型別中:

  • 集合是否為空?
  • 集合中的元素是否為空?
  • 你的方法會不會修改集合?

例如下面這個使用集合引數的Java介面:

interface DataParser<T> {
    void parseData(String input, List<T> output, List<String> errors);
}
複製程式碼

我們的選擇為:

  • List<String>將是非空的,因為呼叫者總是需要接收錯誤資訊。
  • 列表中的元素將是可空的,因為不是每個輸出列表中的條目都有關聯的錯誤資訊。
  • List<String>將是可變的,因為實現程式碼需要向其中新增元素。

那麼Kotlin的實現如下:

class PersonParser : DataParser<Person> {
    override fun parseData(input : String, output : MutableList<Person>, 
        errors : MutableList<String?>)
}
複製程式碼

3.5 物件和基本資料型別的陣列

Kotlin中的一個陣列是一個帶有型別引數的類,其元素型別被指定為相應的型別引數,要在Kotlin中建立陣列,有下面這些方法供你選擇:

  • arrayOf函式建立一個陣列,它包含的元素是指定為該函式的實參
  • arrayOfNulls建立一個給定大小的陣列,包含的是null元素,當然,它只能用來建立包含元素型別可空的陣列
  • Array構造方法接收陣列的大小和一個lambda表示式,呼叫lambda表示式來建立每一個陣列元素,這就是使用非空元素型別來初始化陣列,但不用顯示地傳遞每個元素的方式

Kotlin 知識梳理(7)   Kotlin 的型別系統
執行結果為:
Kotlin 知識梳理(7)   Kotlin 的型別系統

建立沒有裝箱的基本資料型別的陣列

陣列型別的型別引數始終會變成物件型別,因此,如果你宣告瞭一個Array<Int>,它將會是一個包含裝箱整型的陣列,如果你需要建立沒有裝箱的基本資料型別的陣列,必須使用一個基本資料型別陣列的特殊類。

Kotlin提供了若干個獨立的類,每一種基本資料型別對應一個,例如Int型別值的陣列叫作IntArray,要建立一個基本資料型別的陣列,有如下的選擇:

  • 該型別的構造方法接收size引數並返回一個使用對應基本資料型別預設值初始化好的陣列。
  • 工廠函式(例如IntArrayintArrayOf,以及其他陣列型別的函式)接收變長引數的值並建立和儲存這些值的陣列。
  • 另一種構造方法,接收一個大小和一個用來初始化每個元素的lambda

Kotlin 知識梳理(7)   Kotlin 的型別系統
執行結果為:
Kotlin 知識梳理(7)   Kotlin 的型別系統


更多文章,歡迎訪問我的 Android 知識梳理系列:

相關文章