第3章 Kotlin 可空類型與類型系統
跟Java、C和C ++ 一樣, Kotlin也是“靜态類型程式設計語言”。
通常,程式設計語言中的類型系統中定義了
- 如何将數值和表達式歸為不同的類型
- 如何操作這些類型
- 這些類型之間如何互相作用
我們在程式設計語言中使用類型的目的是為了讓編譯器能夠确定類型所關聯的對象需要配置設定多少空間。
類型系統在各種語言之間有非常大的不同,主要的差異存在于編譯時期的文法,以及運作時期的操作實作方式。在每一個程式設計語言中,都有一個特定的類型系統。靜态類型在編譯時期時,就能可靠地發現類型錯誤。是以通常能增進最終程式的可靠性。然而,有多少的類型錯誤發生,以及有多少比例的錯誤能被靜态類型所捕捉,仍有争論。
本章我們簡單介紹一下Kotlin的類型系統。
3.1 類型系統
定型(typing,又稱類型指派)賦予一組比特某個意義。類型通常和存儲器中的數值或對象(如變量)相聯系。因為在電腦中,任何數值都是以一組比特簡單組成的,硬體無法區分存儲器位址、腳本、字元、整數、以及浮點數。類型可以告知程式和程式設計者,應該怎麼對待那些比特。
3.1.1 類型系統的作用
使用類型系統,編譯器可以檢查無意義的、無效的、類型不比對等錯誤代碼。這也正是強類型語言能夠提供更多的代碼安全性保障的原因之一。
另外,靜态類型檢查還可以提供有用的資訊給編譯器。跟動态類型語言相比,由于有了類型的顯式聲明,靜态類型的語言更加易讀好懂。
有了類型我們還可以更好地做抽象化、子產品化的工作。這使得我們可以在較高抽象層次思考并解決問題。例如,Java中的字元數組 char[] s = {'a', 'b', 'c'} 和字元串類型 String str = "abc" 就是最簡單最典型的抽象封裝執行個體。
字元數組
jshell> char[] s = {'a','b','c'}
s ==> char[3] { 'a', 'b', 'c' }
jshell> s[0]
$3 ==> 'a'
jshell> s[1]
$4 ==> 'b'
jshell> s[2]
$5 ==> 'c'
字元串
jshell> String str = "abc"
str ==> "abc"
jshell> str.toCharArray();
$7 ==> char[3] { 'a', 'b', 'c' }
3.1.2 Java的類型系統
Java的類型系統可以簡單用下面的圖來表示:
Java的類型系統
關于Java中的null,有很多比較坑的地方。例如
int i = null; // type mismatch : cannot convert from null to int
short s = null; // type mismatch : cannot convert from null to short
byte b = null: // type mismatch : cannot convert from null to byte
double d = null; //type mismatch : cannot convert from null to double
Integer io = null; // this is ok
int j = io; // this is also ok, but NullPointerException at runtime
存儲方式差別
基本資料類型與引用資料型在建立時,記憶體存儲方式差別如下:
- 基本資料類型在被建立時,在棧上給其劃分一塊記憶體,将數值直接存儲在棧上(性能高)。
- 引用資料型在被建立時,首先在棧上給其引用(句柄)配置設定一塊記憶體,而對象的具體資訊存儲在堆記憶體上,然後由棧上面的引用指向堆中對象的位址。
3.1.3 Kotlin的類型系統
Java是一個近乎純潔的面向對象程式設計語言,但是為了程式設計的友善還是引入了基本資料類型,但是為了能夠将這些基本資料類型當成對象操作,Java為每一個基本資料類型都引入了對應的包裝類型(wrapper class),int的包裝類就是Integer,從Java 5開始引入了自動裝箱/拆箱機制,使得二者可以互相轉換。
Java 為每個原始類型提供了包裝類型:
- 原始類型: boolean,char,byte,short,int,long,float,double
- 包裝類型:Boolean,Character,Byte,Short,Integer,Long,Float,Double
Kotlin中去掉了原始類型,隻有“包裝類型”, 編譯器在編譯代碼的時候,會自動優化性能,把對應的包裝類型拆箱為原始類型。
Kotlin系統類型分為可空類型和不可空類型。Kotlin中引入了可空類型,把有可能為null的值單獨用可空類型來表示。這樣就在可空引用與不可空引用之間劃分出來一條明确的顯式的“界線”。
Kotlin類型層次結構如下圖所示:
Kotlin類型層次結構
通過這樣顯式地使用可空類型,并在編譯期作類型檢查,大大降低了出現空指針異常的機率。
對于Kotlin的數字類型而言,不可空類型與Java中的原始的數字類型對應。如下表所示
Kotlin | Java |
Int | int |
Long | long |
Float | float |
Double | double |
Kotlin中對應的可空數字類型就相當于Java中的裝箱數字類型。如下表所示
Kotlin | Java |
Int? | Integer |
Long? | Long |
Float? | Float |
Double? | Double |
在Java中,從基本數字類型到引用數字類型的轉換就是典型的裝箱操作,例如int轉為Integer。倒過來,從Integer轉為 int 就是拆箱操作。同理,在Kotlin中非空數字類型Int 到可空數字類型Int? 需要進行裝箱操作。 同時,非空的Int類型會被編譯器自動拆箱成基本資料類型 int , 存儲的時候也會存到棧空間。例如下面的代碼,當為Int類型的時候,a === b 傳回的是true; 而當為Int?的時候, a===b 傳回的是false 。
>>> val a: Int = 1000
>>> val b:Int = 1000
>>> a===b
true
>>> a==b
true
上面傳回的都是true,因為a,b它們都是以原始類型存儲的,類似于Java中的基本數字類型。
>>> val a:Int? = 1000
>>> val b:Int? = 1000
>>> a==b
true
>>> a===b
false
我們可以看出,當 a, b 都為可空類型時, a 跟 b 的引用是不等的。
這裡的“等于”号簡單說明如下:
等于符号 | 功能說明 |
= | 指派,在邏輯運算時也有效 |
== | 等于運算,比較的是值,而不是引用 |
=== | 完全等于運算,不僅比較值,而且還比較引用,隻有兩者一緻才為真 |
另外,Java中的數組也是一個較為特殊的類型。這個類型是
T[]
, 這個方括号讓我們覺得不大優雅。Kotlin中摒棄了這個數組類型聲明的文法。Kotlin簡單直接地使用Array類型代表數組類型。這個Array中定義了get, set 算子函數, 同時有一個size 屬性代表數組的長度,還有一個傳回數組元素的疊代子 Iterator<T>的函數iterator()。 完整的定義如下:
public class Array<T> {
public inline constructor(size: Int, init: (Int) -> T)
public operator fun get(index: Int): T
public operator fun set(index: Int, value: T): Unit
public val size: Int
public operator fun iterator(): Iterator<T>
}
其中,構造函數我們可以這麼用
>>> val square = Array(5, { i -> i * i })
>>> square.forEach(::println)
0
1
4
9
16
我們在程式設計過程中常用的boolean[], char[],byte[],short[],int[],long[],float[],double[] ,Kotlin直接使用了8個新的類型來對應這樣的程式設計場景:
BooleanArray
ByteArray
CharArray
DoubleArray
FloatArray
IntArray
LongArray
ShortArray
3.2 可空類型
我想Java和Android開發者肯定早已厭倦了空指針異常(Null Pointer Exception)。這個讨厭的空指針異常在運作時總會在某個你意想不到的地方忽然出現,讓我們感到措手不及。
自然而然地,人們會想到為何不能在編譯時就提前發現這類空指針異常,并大量修複這些問題? 現代程式設計語言正是這麼做的。
Kotlin自然也不例外。
在 Java 8中,我們可以使用 Optional 類型來表達可空的類型。
package com.easy.kotlin;
import java.util.Optional;
import static java.lang.System.out;
public class Java8OptionalDemo {
public static void main(String[] args) {
out.println(strLength(Optional.of("abc")));
out.println(strLength(Optional.ofNullable(null)));
}
static Integer strLength(Optional<String> s) {
return s.orElse("").length();
}
}
運作輸出:
3
0
但是,這樣的代碼,依然不是那麼地優雅。
針對這方面 Groovy 提供了一種安全的屬性/方法通路操作符 ?.
user?.getUsername()?.toUpperCase();
Swift 也有類似的文法, 隻作用在 Optional 的類型上。
Kotlin中使用了Groovy裡面的安全調用符,并簡化了 Optional 類型的使用,直接通過在類型T後面加個?, 就表達了Optional<T>的意義。
上面 Java 8的例子,用 Kotlin 來寫就顯得更加簡單優雅了:
package com.easy.kotlin
fun main(args: Array<String>) {
println(strLength(null))
println(strLength("abc"))
}
fun strLength(s: String?): Int {
return s?.length ?: 0
}
其中,我們使用 String? 同樣表達了 Optional<String>的意思,相比之下,哪個更簡單?一目了然。
還有Java 8 Optional 提供的orElse
s.orElse("").length();
這個東東,在 Kotlin 是最最常見不過的 Elvis 運算符了:
s?.length ?: 0
相比之下,還有什麼理由繼續用 Java 8 的 Optional 呢?
3.3 安全操作符
扔掉Java中的一堆 null 的防禦式樣闆代碼吧!!!
當我們使用Java開發的時候,我們的代碼大多是防禦性的。如果我們不想遇到NullPointerException,我們就需要在使用它之前不停地去判斷它是否為null。
Kotlin正如很多現代程式設計語言一樣——是空安全的。因為我們需要通過一個可空類型符号 T? 來明确地指定一個對象類型 T 是否能為空。
我們可以像這樣去寫:
>>> val str: String = null
error: null can not be a value of a non-null type String
val str: String = null
^
我們可以看到,這裡不能通過編譯, String 類型不能是null 。
一個可以指派為null的String類型的正确姿勢是:String? , 代碼如下所示
>>> var nullableStr: String? = null
>>> nullableStr
null
我們再來看一下Kotlin中關于null的一些有趣的運算。
null跟null是相等的:
>>> null==null
true
>>> null!=null
false
null這個值比較特殊,null 不是Any類型
>>> null is Any
false
但是,null是Any?類型:
>>> null is Any?
true
我們來看看null對應的類型是什麼:
>>> var a=null
>>> a
null
>>> a=1
error: the integer literal does not conform to the expected type Nothing?
a=1
^
從報錯資訊我們可以看出,null的類型是Nothing?。關于Nothing?我們将會在下面的小節中介紹。
3.3.1 安全調用符 ?.
我們不能直接使用可空的nullableStr來調用其屬性或者方法
>>> nullableStr.length
error: only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type String?
nullableStr.length
^
上面的代碼無法編譯, nullableStr可能是null。我們需要使用安全調用符(?.) 來調用
>>> var nullableStr: String? = null
>>> nullableStr?.length
null
>>> nullableStr = "abc"
>>> nullableStr?.length
3
隻有在 nullableStr != null 時才會去調用其length屬性。
3.3.3 非空斷言 !!
如果我們想隻有在確定 nullableStr 不是null的情況下才能這麼調用,否則抛出異常,我們可以使用斷言操作符( !! )
>>> nullableStr = null
>>> nullableStr!!.length
kotlin.KotlinNullPointerException
3.3.2 Elvis運算符 ?:
使用Elvis操作符來給定一個在是null的情況下的替代值
>>> nullableStr
null
>>> var s= nullableStr?:"NULL"
>>> s
NULL
3.4 特殊類型
本節我們介紹Kotlin中的特殊類型:Unit,Nothing,Any以及其對應的可空類型Unit? , Nothing? , Any? 。
3.4.1 Unit類型
Kotlin也是面向表達式的語言。在Kotlin中所有控制流語句都是表達式(除了變量指派、異常等)。
Kotlin中的Unit類型實作了與Java中的void一樣的功能。
總的來說,這個Unit類型并沒有什麼特别之處。它的定義是:
package kotlin
public object Unit {
override fun toString() = "kotlin.Unit"
}
不同的是,當一個函數沒有傳回值的時候,我們用Unit來表示這個特征,而不是null。
大多數時候,我們并不需要顯式地傳回Unit,或者聲明一個函數的傳回類型為Unit。編譯器會推斷出它。
代碼示例:
>>> fun unitExample(){println("Hello,Unit")}
>>> val helloUnit = unitExample()
Hello,Unit
>>> helloUnit
kotlin.Unit
>>> println(helloUnit)
kotlin.Unit
>>> helloUnit is Unit
true
我們可以看出,變量helloUnit的類型是 kotlin.Unit 。
下面幾種寫法是等價的:
@RunWith(JUnit4::class)
class UnitDemoTest {
@Test fun testUnitDemo() {
val ur1 = unitReturn1()
println(ur1) // kotlin.Unit
val ur2 = unitReturn2()
println(ur2) // kotlin.Unit
val ur3 = unitReturn3()
println(ur3) // kotlin.Unit
}
fun unitReturn1() {
}
fun unitReturn2() {
return Unit
}
fun unitReturn3(): Unit {
}
}
跟任何其他類型一樣,它的父類型是Any。如果是一個可空的Unit?,它的父類型是Any?。
Unit類型結構
3.4.2 Nothing與Nothing?類型
在Java中,void不能是變量的類型。也不能被當做值列印輸出。但是,在Java中有個包裝類Void是 void 的自動裝箱類型。如果你想讓一個方法傳回類型 永遠是 null 的話, 可以把傳回類型置為這個大寫的V的Void類型。
代碼示例:
public Void voidDemo() {
System.out.println("Hello,Void");
return null;
}
測試代碼:
@RunWith(JUnit4.class)
public class VoidDemoTest {
@Test
public void testVoid() {
VoidDemo voidDemo = new VoidDemo();
Void v = voidDemo.voidDemo(); // Hello,Void
System.out.println(v); // null
}
}
這個Void對應Kotlin中的Nothing?。它的唯一可被通路到的傳回值也是null。
如上面小節的Kotlin類型層次結構圖所示,在Kotlin類型層次結構的最底層就是類型Nothing。
Nothing的類型層次結構
它的定義如下:
public class Nothing private constructor()
這個Nothing不能被執行個體化
>>> Nothing() is Any
error: cannot access '<init>': it is private in 'Nothing'
Nothing() is Any
^
從上面代碼示例,我們可以看出 Nothing() 不可被通路。
如果一個函數的傳回值是Nothing,這也就意味着這個函數永遠不會有傳回值。
但是,我們可以使用Nothing來表達一個從來不存在的傳回值。例如EmptyList中的 get 函數
internal object EmptyList : List<Nothing>, Serializable, RandomAccess {
override fun get(index: Int): Nothing = throw IndexOutOfBoundsException("Empty list doesn't contain element at index $index.")
}
}
一個空的List調用get函數, 直接是抛出了IndexOutOfBoundsException ,這個時候我們就可以使用Nothing 作為這個get函數的傳回類型,因為它永遠不會傳回某個值,而是直接抛出了異常。
再例如, Kotlin的标準庫裡面的exitProcess函數:
@file:kotlin.jvm.JvmName("ProcessKt")
@file:kotlin.jvm.JvmVersion
package kotlin.system
@kotlin.internal.InlineOnly
public inline fun exitProcess(status: Int): Nothing {
System.exit(status)
throw RuntimeException("System.exit returned normally, while it was supposed to halt JVM.")
}
注意:Unit與Nothing之間的差別: Unit類型表達式計算結果的傳回類型是Unit。Nothing類型的表達式計算結果是永遠不會傳回的(跟Java中的void相同)。
Nothing?可以隻包含一個值:null。代碼示例:
>>> var nul:Nothing?=null
>>> nul = 1
error: the integer literal does not conform to the expected type Nothing?
nul = 1
^
>>> nul = true
error: the boolean literal does not conform to the expected type Nothing?
nul = true
^
>>> nul = null
>>> nul
null
從上面的代碼示例,我們可以看出:Nothing?它唯一允許的值是null,被用作任何可空類型的空引用。
3.4.3 Any與Any?類型
就像Any是在非空類型層次結構的根,Any?是可空類型層次的根。
Any?是Any的超集,Any?是Kotlin的類型層次結構的最頂端。
Any與Any?類型
代碼示例:
>>> 1 is Any
true
>>> 1 is Any?
true
>>> null is Any
false
>>> null is Any?
true
>>> Any() is Any?
true
3.5 類型檢測與類型轉換
3.5.1 is運算符
is運算符可以檢查對象是否與特定的類型相容(此對象是該類型,或者派生于該類型)。
is運算符用來檢查一個對象(變量)是否屬于某資料類型(如Int、String、Boolean等)。C#裡面也有這個運算符。
is
運算符類似Java的
instanceof
:
jshell> "abc" instanceof String
$10 ==> true
在Kotlin中,我們可以在運作時通過使用
is
操作符或其否定形式
!is
來檢查對象是否符合給定類型:
>>> "abc" is String
true
>>> "abc" !is String
false
>>> null is Any
false
>>> null is Any?
true
代碼示例:
@RunWith(JUnit4::class)
class ISTest {
@Test fun testIS() {
val foo = Foo()
val goo = Goo()
println(foo is Foo) //true 自己
println(goo is Foo)// 子類 is 父類 = true
println(foo is Goo)//父類 is 子類 = false
println(goo is Goo)//true 自己
}
}
open class Foo
class Goo : Foo()
3.5.2 類型自動轉換
在Java代碼中,當我們使用
str instanceof String
來判斷其值為
true
的時候,我們想使用str變量,還需要顯式的強制轉換類型:
@org.junit.runner.RunWith(org.junit.runners.JUnit4.class)
public class TypeSystemDemo {
@org.junit.Test
public void testVoid() {
Object str = "abc";
if (str instanceof String) {
int len = ((String)str).length(); // 顯式的強制轉換類型為String
println(str + " is instanceof String");
println("Length: " + len);
} else {
println(str + " is not instanceof String");
}
boolean is = "1" instanceof String;
println(is);
}
void println(Object obj) {
System.out.println(obj);
}
}
而大多數情況下,我們不需要在 Kotlin 中使用顯式轉換操作符,因為編譯器跟蹤不可變值的 is-檢查,并在需要時自動插入(安全的)轉換:
@Test fun testIS() {
val len = strlen("abc")
println(len) // 3
val lens = strlen(1)
println(lens) // 1
}
fun strlen(ani: Any): Int {
if (ani is String) {
return ani.length
} else if (ani is Number) {
return ani.toString().length
} else if (ani is Char) {
return 1
} else if (ani is Boolean) {
return 1
}
print("Not A String")
return -1
}
3.5.3 as運算符
as運算符用于執行引用類型的顯式類型轉換。如果要轉換的類型與指定的類型相容,轉換就會成功進行;如果類型不相容,使用
as?
運算符就會傳回值null。
代碼示例:
>>> open class Foo
>>> class Goo:Foo()
>>> val foo = Foo()
>>> val goo = Goo()
>>> foo as Goo
java.lang.ClassCastException: Line69$Foo cannot be cast to Line71$Goo
>>> foo as? Goo
null
>>> goo as Foo
Line71$Goo@73dce0e6
我們可以看出,在Kotlin中,父類是禁止轉換為子類型的。按照Liskov替換原則,父類轉換為子類是對OOP的嚴重違反,不提倡、也不建議。嚴格來說,父類是不能轉換為子類的,子類包含了父類所有的方法和屬性,而父類則未必具有和子類同樣成員範圍,是以這種轉換是不被允許的,即便是兩個具有父子關系的空類型,也是如此。
本章小結
Kotlin通過引入可空類型,在編譯時就大量“清掃了”空指針異常。同時,Kotlin中還引入了安全調用符(?.) 以及Elvis操作符( ?: ) , 使得我們的代碼寫起來更加簡潔。
Kotlin的類型系統比Java更加簡單一緻,Java中的原始類型與數組類型在Kotlin中都統一表現為引用類型。
Kotlin中還引入了Unit,Nothing等特殊類型,使得沒有傳回值的函數與永遠不會傳回的函數有了更加規範一緻的簽名。