Kotlin極簡教程:第4章 基本數據類型與類型系統

原文鏈接:https://github.com/EasyKotlin

到目前爲止,我們已經瞭解了Kotlin的基本符號以及基礎語法。我們可以看出,使用Kotlin寫的代碼更簡潔、可讀性更好、更富有生產力。

本章我們來學習一下Kotlin的基本數據類型與類型系統。

道生一,一生二,二生三,三生萬物 (老子《道德經》第四十二章)

在計算機科學中,最早的類型系統用來區別數字裏面的整數和浮點數。

在20世紀五六十年代,這種分類擴展到了結構化的數據和高階函數中。

70年代,引入了幾個更爲豐富的概念,例如:參數化類型,抽象數據類型,模塊系統,子類型等等,類型系統作爲一個獨立的領域形成了。

在每一門編程語言中,都有一個特定的類型系統(Type System)。類型系統是一門編程語言最核心也是最基礎的部分。我們這裏說的類型系統,可以簡單理解爲以下兩個部分:

  • 一組基本類型構成的PTS(Primary Type Set,基本類型集合);
  • PTS上定義的一系列組合、運算、轉換規則等。

這一簡單優雅而驚人的世界構成觀,貫穿了人類現實世界和計算機編程語言所定義的虛擬世界。或許語言的設計者也沒有料想到,但是最終的結果確實是有限的設計導出了無限的可能性。

本章我們將學習Kotlin語言的基本類型,以及簡單介紹Kotlin的類型系統。

4.1 什麼是類型?

一切皆是映射

在計算機中,任何數值都是以一組比特(01)組成的,硬件無法區分內存地址、腳本、字符、整數、以及浮點數。這個時候,我們使用類型賦予一組比特以特定的意義。

類型(Type),本質上就是內存中的數值或變量對象的邏輯映射。

《周易》有云:

易有太極,是生兩儀,兩儀生四象,四象生八卦。(《易傳·繫辭上傳》) 。

這裏所包含的思想,跟我們這裏所說的類型系統的思想有着異曲同工之妙。

類型系統用於定義如何將編程語言中的數值和表達式歸類爲許多不同的類型,如何操作這些類型,這些類型如何互相作用等。

類型系統在各種語言之間有非常大的不同,主要的差異存在於編譯時期的語法,以及運行時期的操作實現方式。

類型系統提供的主要功能有:

  • 安全性

編譯器可以使用類型來檢查無意義的,或者是可能無效的代碼。例如,在強類型的語言中,如果沒有對字符串的+進行重載,那麼表達式

"Hello, World" + 3

就會被編譯器檢測出來,因爲不能對字符串加上一個整數。強類型提供更多的安全性。

但是,爲了讓程序員可以寫出極簡的代碼,很多語言都提供了操作符重載的機制。比如說,在Scala中,上面的代碼是可以被正確執行的(重載了+操作符)

scala> "Hello,World"+1
res15: String = Hello,World1

scala> 1+"Hello,World"
res16: String = 1Hello,World

但是在Kotlin中, 由於Int類型沒有對+實現重載,所以情況是這樣

>>> "Hello,World"+1
Hello,World1
>>> 1+"Hello,World"
error: none of the following functions can be called with the arguments supplied: 
public final operator fun plus(other: Byte): Int defined in kotlin.Int
public final operator fun plus(other: Double): Double defined in kotlin.Int
public final operator fun plus(other: Float): Float defined in kotlin.Int
public final operator fun plus(other: Int): Int defined in kotlin.Int
public final operator fun plus(other: Long): Long defined in kotlin.Int
public final operator fun plus(other: Short): Int defined in kotlin.Int
1+"Hello,World"
 ^
  • 最優化

靜態類型檢查可提供有用的信息給編譯器。編譯器可以使用更有效率的機器指令,實現編譯器優化。

  • 可讀性

  • 抽象化(或模塊化)

類型本質上是對較低層次的邏輯單元進行高層次的邏輯抽象。這樣我們就可以直接使用類型在較高層次的方式思考,而不是繁重的低層次實現。

例如,我們可以將字符串想成一個值,以此取代僅僅是字節的數組。字符串就是一個抽象數據類型。

從01到類型,從類型到接口API,再到軟件服務,都可以看做是廣義的“類型”範疇。

程序中的變量在程序執行期間,可能會有不同的取值範圍,我們可以把變量可取值的最大範圍稱爲這個變量的類型。例如,具有類型Boolean的變量x,在程序執行期間,只能取布爾值。指定變量類型的程序設計語言,稱爲類型化的語言(typed language)。

如果一個語言,不限制變量的取值,稱爲無類型語言(untyped language),我們既可以說它不具有類型,也可以說它具有一個通用類型,這個類型的取值範圍是程序中所有可能的值。

類型系統是類型化語言的一個組成部分,它用來計算和跟蹤程序中所有表達式的類型,從而判斷某段程序是否表現良好(well behaved)。

如果程序語言的語法中含有類型標記,就稱該語言是顯式類型化的(explicitly typed),否則就稱爲隱式類型化的(implicitly typed)。

像C、C++、Java等語言,都是顯式類型化的。而像ML、Haskell、Groovy等可以省略類型聲明,它們的類型系統會自動推斷出程序的類型。

4.2 編譯時類型與運行時類型

Koltin是一門強類型的、靜態類型、支持隱式類型的顯式類型語言。

4.2.1 弱類型(Weakly checked language)與強類型(Strongly checked language)

類型系統最主要的作用是,通過檢查類型的運算和轉換過程,來減少類型錯誤的發生。如果一個語言的編譯器引入越多的類型檢查的限制,就可以稱這個語言的類型檢查越強,反之越弱。根據類型檢查的強弱,我們把編程語言分爲

  • 弱類型語言
  • 強類型語言

弱類型語言在運行時會隱式做數據類型轉換。
強類型語言在運行時會確保不會發生未經明確轉換(顯式調用)的類型轉換。

但是另一方面,強和弱只是相對的。

Kotlin是強類型語言。

4.2.2 靜態類型(Statically checked language)與動態類型(Dynamically

checked language)

類型檢查可發生在編譯時期(靜態檢查)或運行時期(動態檢查)。這樣我們將編程語言分爲

  • 靜態類型語言
  • 動態類型語言

靜態類型檢查是基於編譯器來分析源碼本身來確保類型安全。靜態類型檢查能讓很多bug在編碼早期被捕捉到,並且它也能優化運行。因爲如果編譯器在編譯時已經證明程序是類型安全的,就不用在運行時進行動態的類型檢查,編譯過後的代碼會更優化,運行更快。

動態類型語言是在運行時期進行類型標記的檢查,因爲變量所約束的值,可經由運行路徑獲得不同的標記。關於動態類型,有個很形象的說法:

當看到一隻鳥走起來像鴨子、游泳起來像鴨子、叫起來也像鴨子,那麼這隻鳥就可以被稱爲鴨子。——詹姆斯·惠特科姆·萊利(James Whitcomb Riley,1849-1916)

Kotlin是靜態類型語言。

4.2.3 顯式類型(Explicitly typed language)與隱式類型(Implicitly typed language)

還有一種區分方法是,根據變量名是否需要顯式給出類型的聲明,來將語言分爲

  • 顯式類型語言
  • 隱式類型語言

前者需要在定義變量時顯式給出變量的類型,而後者可以使用類型推論來確定變量的類型。

大多數靜態類型語言,例如 Java、C/C++ 都是顯式類型語言。但是有些則不是,如 Haskell、ML 等,它們可以基於變量的操作來推斷其類型;

Scala 是靜態類型語言,它使用類型推斷功能來支持隱式類型。

Kotlin 跟Scala類似,它也使用類型推斷支持隱式類型。但是,在一些場景下也需要顯式聲明變量的類型,所以我們可以說,同時也是顯式類型。

4.3 根類型Any

Kotlin 中所有類都有一個共同的超類 Any ,如果類聲明時沒有指定超類,則默認爲 Any 。我們來看一段代碼:

>>> val any = Any()
>>> any
java.lang.Object@2e377400
>>> any::class
class kotlin.Any
>>> any::class.java
class java.lang.Object

也就是說,Any在運行時,其類型自動映射成java.lang.Object。我們知道,在Java中Object類是所有引用類型的父類。但是不包括基本類型:byte int long等,基本類型對應的包裝類是引用類型,其父類是Object。而在Kotlin中,直接統一——所有類型都是引用類型,統一繼承父類Any

Any是Java的等價Object類。但是跟Java不同的是,Kotlin中語言內部的類型和用戶定義類型之間,並沒有像Java那樣劃清界限。它們是同一類型層次結構的一部分。

Any 只有 equals() 、 hashCode() 和 toString() 三個方法。其源碼是

public open class Any {
    /**
     * Indicates whether some other object is "equal to" this one. Implementations must fulfil the following
     * requirements:
     *
     * * Reflexive: for any non-null reference value x, x.equals(x) should return true.
     * * Symmetric: for any non-null reference values x and y, x.equals(y) should return true if and only if y.equals(x) returns true.
     * * Transitive:  for any non-null reference values x, y, and z, if x.equals(y) returns true and y.equals(z) returns true, then x.equals(z) should return true
     * * Consistent:  for any non-null reference values x and y, multiple invocations of x.equals(y) consistently return true or consistently return false, provided no information used in equals comparisons on the objects is modified.
     *
     * Note that the `==` operator in Kotlin code is translated into a call to [equals] when objects on both sides of the
     * operator are not null.
     */
    public open operator fun equals(other: Any?): Boolean

    /**
     * Returns a hash code value for the object.  The general contract of hashCode is:
     *
     * * Whenever it is invoked on the same object more than once, the hashCode method must consistently return the same integer, provided no information used in equals comparisons on the object is modified.
     * * If two objects are equal according to the equals() method, then calling the hashCode method on each of the two objects must produce the same integer result.
     */
    public open fun hashCode(): Int

    /**
     * Returns a string representation of the object.
     */
    public open fun toString(): String
}

4.3.1 對象相等性

從Any的源碼註釋中,我們可以看到,判斷兩個對象是否相等,需要滿足以下條件:

  • 自反性:對於任何非空引用值x,x.equals(x) 應返回true。
  • 對稱性:對於任何非空引用值x和y,x.equals(y) 應返回true當且僅當y.equals(x) 返回true。
  • 傳遞性:對於任何非空引用值x,y,z,如果x.equals(y) 返回true,y.equals(z) 返回true,那麼x.equals(z) 應返回true
  • 一致性:對於任何非空引用值x和y,多次調用x.equals(y) 始終返回true或者始終返回false。

另外,在Kotlin中,操作符==會被編譯器翻譯成調用equals() 函數。

4.4 基本類型(Primitive Types)

本節我們來探討學習:Kotlin的基礎類型:數字、字符、布爾和數組等。

我們知道Java的類型分成兩種:一種是基本類型,一種是引用類型。它們的本質區別是:

基本類型是在堆棧處分配空間存“值”,而引用類型是在堆裏面分配空間存“值”。

Java的基本類型有: byte、int、short、long、float、double、char、boolean,這些類都有對應的裝箱類(引用類型)。

另外,void也可以算是一種特殊的基本類型,它也有一個裝箱類Void(跟我們後文講到的Unit、Nothing相關)。因爲,Void是不能new出來的,也就是不能在堆裏面分配空間存對應的值。所以,Void是一開始在堆棧處分配好空間。所以,將Void歸成基本類型。

在Kotlin中,一切皆是對象。所有類型都是引用類型。沒有類似Java中的基本類型。但是,可以把Kotlin中對應的這幾種基本數據類型,理解爲Java的基本類型的裝箱類。

Integer.java

public final class Integer extends Number implements Comparable<Integer> {
    /**
     * A constant holding the minimum value an {@code int} can
     * have, -2<sup>31</sup>.
     */
    @Native public static final int   MIN_VALUE = 0x80000000;

    /**
     * A constant holding the maximum value an {@code int} can
     * have, 2<sup>31</sup>-1.
     */
    @Native public static final int   MAX_VALUE = 0x7fffffff;

    /**
     * The {@code Class} instance representing the primitive type
     * {@code int}.
     *
     * @since   JDK1.1
     */
    @SuppressWarnings("unchecked")
    public static final Class<Integer>  TYPE = (Class<Integer>) Class.getPrimitiveClass("int");

    ...

}

Kotlin中的Int類型:

public class Int private constructor() : Number(), Comparable<Int> {
    companion object {
        /**
         * A constant holding the minimum value an instance of Int can have.
         */
        public const val MIN_VALUE: Int = -2147483648

        /**
         * A constant holding the maximum value an instance of Int can have.
         */
        public const val MAX_VALUE: Int = 2147483647
    }
    ...
}

我們通過Java的Integer封裝類,跟Kotlin的Int類的定義可以看出兩者的思想上的同源性。

Kotlin的基本類型的類圖結構如下圖所示

Kotlin極簡教程

4.4.1 數字(Number)類型

Kotlin 提供瞭如下的內置類型來表示數字(與 Java 很相近):

類型 寬度(Bit)
Double 64
Float 32
Long 64
Int 32
Short 16
Byte 8

從上面的Kotlin的基本類型的類的結構圖,我們可以看出這些內置的數據類型,都繼承了NumberComparable類。例如,Byte類型的聲明:

public class Byte private constructor() : Number(), Comparable<Byte> {
    ...
}

Kotlin 的數字類型跟 Java 基本相同。有一點不同的是,Kotlin 對於數字沒有隱式拓寬轉換(如 Java 中 int 可以隱式轉換爲long)。

注意在 Kotlin 中字符Char不是數字。這些基本數據類型,會在運行時自動優化爲Java的double、float、long、int、short、byte。

字面常量值(literal constant values)

數值常量字面值有以下幾種:

  • 十進制: 123
  • Long 類型用大寫 L 標記: 123L
  • 十六進制: 0x0F
  • 二進制: 0b00001011

代碼示例:

>>> 123
123
>>> 123::class
class kotlin.Int
>>> 123::class.java
int
>>> 123L
123
>>> 123L::class
class kotlin.Long
>>> 123L::class.java
long

>>> val b:Byte=128
error: the integer literal does not conform to the expected type Byte
val b:Byte=128
           ^

>>> val b:Byte=127
>>> b::class
class kotlin.Byte
>>> b::class.java
byte

>>> 0x0f
15
>>> 0x0F
15
>>> 0b1000
8

同樣的,當我們賦值超過變量的類型的取值範圍時,編譯器會直接拋錯。

注意: 不支持八進制

Kotlin 同樣支持浮點數的常規表示方法:

  • 默認 double:123.5123.5e10
  • Float 用 f 或者 F 標記: 123.5f

代碼示例:

>>> 1234.5
1234.5
>>> 1234.5::class
class kotlin.Double
>>> 1234.5::class.java
double
>>> 12.3e10
1.23E11
>>> 12.3e10::class
class kotlin.Double
>>> 456.7f
456.7
>>> 456.7f::class
class kotlin.Float
>>> 456.7f::class.java
float

我們也可以使用數字字面值中的下劃線(自 1.1 起),使數字常量更易讀:

>>> 1_000_000
1000000
>>> 1234_5678_9012_3456L
1234567890123456
>>> 0xFF_EC_DE_5E
4293713502
>>> 0b11010010_01101001_10010100_10010010
3530134674

在 Java 平臺數字是物理存儲爲 JVM 的原生類型,除非我們需要一個可空的引用(如 Int?)或泛型。
後者情況下會把數字裝箱。

顯式轉換

由於不同的表示方式,值範圍較小類型並不是較大類型的子類型,是不能隱式轉換的。

代碼示例:

>>> val a: Int? = 1
>>> val b: Long? = a
error: type mismatch: inferred type is Int? but Long? was expected
val b: Long? = a
               ^

>>> val b: Byte = 1
>>> val i: Int = b
error: type mismatch: inferred type is Byte but Int was expected
val i: Int = b
             ^

這意味着在不進行顯式轉換的情況下我們不能把 Int 型值賦給一個 Long 變量。也不能把 Byte 型值賦給一個 Int 變量。

我們可以顯式轉換來拓寬數字

>>> val i: Int = b.toInt() // OK: 顯式拓寬

每個數字類型都繼承Number抽象類,其中定義瞭如下的轉換函數:

toDouble(): Double
toFloat(): Float
toLong(): Long
toInt(): Int
toChar(): Char
toShort(): Short
toByte(): Byte

所以,在數字之間的轉換,我們直接調用上面的這些轉換函數即可。

運算符+重載

缺乏隱式類型轉換並不顯著,因爲類型會從上下文推斷出來,而算術運算會有重載做適當轉換,例如:

val l = 1L + 3 // Long + Int => Long

這個是通過運算符+重載實現的。我們可以在Long類的源代碼中看到這個plus 運算符函數的定義:

public operator fun plus(other: Byte): Long
public operator fun plus(other: Short): Long
public operator fun plus(other: Int): Long
public operator fun plus(other: Long): Long
public operator fun plus(other: Float): Float
public operator fun plus(other: Double): Double

也就是說, 編譯器會把1L + 3 翻譯成 1L.plus(3),然後這個傳入的參數類型必須是Byte、Short、Int、Long、Float、Double中的一種。例如,我們傳入一個字符Char參數,編譯器就會直接拋錯:

>>> 'a'
a
>>> 'a'::class
class kotlin.Char
>>> 'a'::class.java
char
>>> 1L+'a'
error: none of the following functions can be called with the arguments supplied: 
public final operator fun plus(other: Byte): Long defined in kotlin.Long
public final operator fun plus(other: Double): Double defined in kotlin.Long
public final operator fun plus(other: Float): Float defined in kotlin.Long
public final operator fun plus(other: Int): Long defined in kotlin.Long
public final operator fun plus(other: Long): Long defined in kotlin.Long
public final operator fun plus(other: Short): Long defined in kotlin.Long
1L+'a'
  ^

運算

Kotlin支持數字運算的標準集,運算被定義爲相應的類成員(但編譯器會將函數調用優化爲相應的指令)。

對於位運算,沒有特殊字符來表示,而只可用中綴方式調用命名函數(infix fun),例如:

val x = (1 shl 2) and 0x000FF000

這是完整的位運算列表(只用於 IntLong):

  • shl(bits) – 有符號左移 (Java 的 <<)
  • shr(bits) – 有符號右移 (Java 的 >>)
  • ushr(bits) – 無符號右移 (Java 的 >>>)
  • and(bits) – 位與
  • or(bits) – 位或
  • xor(bits) – 位異或
  • inv() – 位非

4.4.2 Char: 字符(Character)類型與轉義符(Escape character)

字符用 Char 類型表示。它們不能直接當作數字

fun check(c: Char) {
    if (c == 1) { // 錯誤:類型不兼容
        // ……
    }
}

字符字面值用 單引號 括起來: '1'
特殊字符可以用反斜槓轉義。

Kotlin支持如下轉義字符:

\t
\b
\n
\r
\`
\"
\\
\$

編碼其他字符要用 Unicode 轉義序列語法,例如:'\uFF00'

Char類的函數接口定義如下:

public class Char private constructor() : Comparable<Char> {
    /**
     * Compares this value with the specified value for order.
     * Returns zero if this value is equal to the specified other value, a negative number if it's less than other,
     * or a positive number if it's greater than other.
     */
    public override fun compareTo(other: Char): Int

    /** Adds the other Int value to this value resulting a Char. */
    public operator fun plus(other: Int): Char

    /** Subtracts the other Char value from this value resulting an Int. */
    public operator fun minus(other: Char): Int
    /** Subtracts the other Int value from this value resulting a Char. */
    public operator fun minus(other: Int): Char

    /** Increments this value. */
    public operator fun inc(): Char
    /** Decrements this value. */
    public operator fun dec(): Char

    /** Creates a range from this value to the specified [other] value. */
    public operator fun rangeTo(other: Char): CharRange

    /** Returns the value of this character as a `Byte`. */
    public fun toByte(): Byte
    /** Returns the value of this character as a `Char`. */
    public fun toChar(): Char
    /** Returns the value of this character as a `Short`. */
    public fun toShort(): Short
    /** Returns the value of this character as a `Int`. */
    public fun toInt(): Int
    /** Returns the value of this character as a `Long`. */
    public fun toLong(): Long
    /** Returns the value of this character as a `Float`. */
    public fun toFloat(): Float
    /** Returns the value of this character as a `Double`. */
    public fun toDouble(): Double
}

我們來用代碼示例這些函數的使用:

如果兩個字符相等:

>>> 'a'.compareTo('a')
0

如果兩個字符不相等:

>>> 'a'.compareTo('b')
-1
>>> 'a'.compareTo('c')
-1
>>> 'b'.compareTo('a')
1
>>> 'c'.compareTo('a')
1

Char字符只重載了加上Int類型的數字的+運算符:

>>> 'a'+1
b

>>> 'a'+1L
error: the integer literal does not conform to the expected type Int
'a'+1L

所以,當我們把一個Char類型值和不是Int類型的值相加,就報錯了。

相減:

>>> 'a'-1
`
>>> 'c'-'a'
2

自增計算:

>>> var a='a'
>>> val b=a++
>>> a
b
>>> b
a
>>> val c=++a
>>> c
c

我們不能在字符的字面量上直接使用++:

>>> 'a'++
error: variable expected
'a'++
^

>>> ++'a'
error: variable expected
++'a'
  ^

範圍

>>> 'a'.rangeTo('z')
a..z
>>> for(c in 'a'..'z') {print(c)}

abcdefghijklmnopqrstuvwxyz

Char的顯式類型轉換函數如下:

/** Returns the value of this character as a `Byte`. */
public fun toByte(): Byte
/** Returns the value of this character as a `Char`. */
public fun toChar(): Char
/** Returns the value of this character as a `Short`. */
public fun toShort(): Short
/** Returns the value of this character as a `Int`. */
public fun toInt(): Int
/** Returns the value of this character as a `Long`. */
public fun toLong(): Long
/** Returns the value of this character as a `Float`. */
public fun toFloat(): Float
/** Returns the value of this character as a `Double`. */
public fun toDouble(): Double

例如,我們顯式把字符轉換爲 Int 數字:

fun decimalDigitValue(c: Char): Int {
    if (c !in '0'..'9')
        throw IllegalArgumentException("Out of range")
    return c.toInt() - '0'.toInt() // 顯式轉換爲數字
}

測試代碼:

>>> decimalDigitValue('a')
java.lang.IllegalArgumentException: Out of range
    at Line24.decimalDigitValue(Unknown Source)

>>> decimalDigitValue('1')
1

4.4.3 Boolean: 布爾類型

Kotlin的布爾類型用 Boolean 類來表示,它有兩個值:truefalse

>>> true::class
class kotlin.Boolean
>>> true::class.java
boolean

對應Java中的boolean類型。

其源碼定義如下:

package kotlin

/**
 * Represents a value which is either `true` or `false`. On the JVM, non-nullable values of this type are
 * represented as values of the primitive type `boolean`.
 */
public class Boolean private constructor() : Comparable<Boolean> {
    /**
     * Returns the inverse of this boolean.
     */
    public operator fun not(): Boolean

    /**
     * Performs a logical `and` operation between this Boolean and the [other] one.
     */
    public infix fun and(other: Boolean): Boolean

    /**
     * Performs a logical `or` operation between this Boolean and the [other] one.
     */
    public infix fun or(other: Boolean): Boolean

    /**
     * Performs a logical `xor` operation between this Boolean and the [other] one.
     */
    public infix fun xor(other: Boolean): Boolean

    public override fun compareTo(other: Boolean): Int
}

從上面我們可以看出,Boolean類的內置的布爾運算有:

  • ! 邏輯非 not()

  • && 短路邏輯與 and()

  • || 短路邏輯或or()

  • xor 異或(相同false,不同true)

另外,Boolean還繼承實現了ComparablecompareTo()函數。

代碼示例:

>>> !true
false
>>> true.not()
false
>>> true && true
true
>>> true.and(false)
false
>>> true || false
true
>>> false.or(false)
false
>>> true xor true
false
>>> true xor false
true
>>> false xor false
false
>>> true > false
true
>>> true < false
false
>>> true.compareTo(false)
1
>>> true.compareTo(false)
1
>>> true.compareTo(true)
0
>>> false.compareTo(true)
-1

4.4.4 String: 字符串類型

Kotlin的字符串用 String 類型表示。對應Java中的java.lang.String。字符串是不可變的。

>>> "abc"::class
class kotlin.String
>>> "abc"::class.java
class java.lang.String

另外,在Kotlin中,String同樣是final不可繼承的。

代碼示例:

>>> class MyString:String
error: this type is final, so it cannot be inherited from
class MyString:String
               ^

索引運算符 s[i]

字符串的元素——字符可以使用索引運算符 s[i]來訪問。

>>> val s="abc"
>>> s
abc
>>> s[0]
a

當我們下標越界時,會拋越界錯誤:

>>> s[-1]
java.lang.StringIndexOutOfBoundsException: String index out of range: -1
    at java.lang.String.charAt(String.java:646)

>>> s[3]
java.lang.StringIndexOutOfBoundsException: String index out of range: 3
    at java.lang.String.charAt(String.java:646)

從出錯信息,我們可以看出,索引運算符 s[i]會被翻譯成java.lang.String.charAt(), 背後調用的是Java的String類。其調用的方法是:

public char charAt(int index) {
    if ((index < 0) || (index >= value.length)) {
        throw new StringIndexOutOfBoundsException(index);
    }
    return value[index];
}

for 循環迭代字符串

我們可以用 for 循環迭代字符串:

>>> for(c in "abc") { println(c)  }
a
b
c

關於字符串String類的完整的操作方法,我們可以看下源碼:

public class String : Comparable<String>, CharSequence {
    companion object {}

    /**
     * Returns a string obtained by concatenating this string with the string representation of the given [other] object.
     */
    public operator fun plus(other: Any?): String

    public override val length: Int

    public override fun get(index: Int): Char

    public override fun subSequence(startIndex: Int, endIndex: Int): CharSequence

    public override fun compareTo(other: String): Int
}

類似的,字符串有一個length屬性:

>>> "abc".length
3

重載+操作符

字符串類重載了+操作符,作用對象可以是任何對象,包括空引用:

>>> "abc".plus(true)
abctrue
>>> "abc"+false
abcfalse
>>> "abc"+1
abc1
>>> "abc"+1.20
abc1.2
>>> "abc"+100L
abc100
>>> "abc"+"cdef"
abccdef
>>> "abc"+null
abcnull
>>> "abc"+'z'
abcz
>>> "abc"+arrayOf(1,2,3,4,5)
abc[Ljava.lang.Integer;@3d6f0054

截取字符串的子串:

>>> "abc".subSequence(0,1)
a
>>> "abc".subSequence(0,2)
ab
>>> "abc".subSequence(0,3)
abc
>>> "abc".subSequence(0,4)
java.lang.StringIndexOutOfBoundsException: String index out of range: 4
    at java.lang.String.substring(String.java:1951)
    at java.lang.String.subSequence(String.java:1991)

字符串字面值

字符串的字面值,可以包含原生字符串可以包含換行和任意文本,也可以是帶有轉義字符(Escape Charactor)的轉義字符串。

>>> val s = "Hello,World!\n\n\n"
>>> s
Hello,World!

>>> 

轉義採用傳統的反斜槓方式。

原生字符串使用三個引號(""")分界符括起來,內部沒有轉義並且可以包含換行和任何其他字符:

>>> val text = """
...     for (c in "abc")
...         print(c)
... """
>>> text

    for (c in "foo")
        print(c)

>>> 

另外,在package kotlin.text下面的Indent.kt代碼中,Kotlin還定義了String類的擴展函數:

fun String.trimMargin(marginPrefix: String = "|"): String
fun String.trimIndent(): String

我們可以使用trimMargin()trimIndent() 裁剪函數來去除前導空格。可以看出,trimMargin()函數默認使用 "|" 來作爲邊界字符:

>>> val text = """
... |理論是你知道是這樣,但它卻不好用。
... |實踐是它很好用,但你不知道是爲什麼。
... |程序員將理論和實踐結合到一起:
... |既不好用,也不知道是爲什麼。
...     """
>>> text.trimMargin()
理論是你知道是這樣,但它卻不好用。
實踐是它很好用,但你不知道是爲什麼。
程序員將理論和實踐結合到一起:
既不好用,也不知道是爲什麼。

默認 | 用作邊界前綴,但你可以選擇其他字符並作爲參數傳入,比如 trimMargin(">")

trimIndent()函數,則是把字符串行的左邊空白對齊切割:

>>> val text="""
...              Hello
...                    World!
... """
>>> text.trimIndent()
Hello
      World!
>>> val text="""
...             Hello,
...         World!
... """
>>> text.trimIndent()
    Hello,
World!

字符串模板

字符串可以包含模板表達式,即一些小段代碼,會求值並把結果合併到字符串中。
模板表達式以美元符($)開頭,由一個簡單的名字構成:

>>> val h=100
>>> val str = "A hundred is $h"
>>> str
A hundred is 100

或者用花括號擴起來的任意表達式:

>>> val s = "abc"
>>> val str = "$s.length is ${s.length}"
>>> str
abc.length is 3

原生字符串和轉義字符串內部都支持模板。

>>> val price=9.9
>>> val str="""Price is $$price"""
>>> str
Price is $9.9
>>> val str="Price is $$price"
>>> str
Price is $9.9

>>> val quantity=100
>>> val str="Quantity is $quantity"
>>> str
Quantity is 100
>>> val str="""Quantity is $quantity"""
>>> str
Quantity is 100

4.4.5 Array: 數組類型

數組在 Kotlin 中使用 Array 類來表示,它定義了 getset 函數(映射到重載運算符 [])和 size 屬性,以及一個用於變量數組的iterator()函數:

class Array<T> private constructor() {
    val size: Int
    operator fun get(index: Int): T
    operator fun set(index: Int, value: T): Unit
    operator fun iterator(): Iterator<T>
    // ……
}

我們可以使用函數 arrayOf() 來創建一個數組並傳遞元素值給它。這個函數簽名如下:

public inline fun <reified @PureReifiable T> arrayOf(vararg elements: T): Array<T>

其中,vararg表示是一個參數個數是一個變量。

例如, arrayOf(1, 2, 3) 創建了 array [1, 2, 3] :

>>> arrayOf(1,2,3)
[Ljava.lang.Integer;@4a37191a
>>> arrayOf(1,2,3)::class
class kotlin.Array
>>> arrayOf(1,2,3)::class.java
class [Ljava.lang.Integer;

另外,Kotlin還允許不同類型元素放到一個數組中,例如:

>>> val arr = arrayOf(1,"2",true)
>>> arr
[Ljava.lang.Object;@61af1510
>>> arr.forEach{ println(it) }
1
2
true
>>> arr.forEach{ println(it::class) }
class kotlin.Int
class kotlin.String
class kotlin.Boolean

Kotlin自動把這個數組元素的類型升級爲java.lang.Object, 同時,由於Kotlin擁有的類型推斷的功能,我們仍然可以看到每個數組元素對應的各自的類型。

函數 arrayOfNulls() 可以用於創建一個指定大小、元素都爲空的數組。這個特殊的空數組在創建的時候,我們需要指定元素的類型。如果不指定,直接按照下面這樣寫,會報錯:

>>> arrayOfNulls(10)
error: type inference failed: Not enough information to infer parameter T in fun <reified T> arrayOfNulls(size: Int): Array<T?>
Please specify it explicitly.

arrayOfNulls(10)
^

也就是說,我們要指定

>>> arrayOfNulls<Int>(10)
[Ljava.lang.Integer;@77c10a5f
>>> arrayOfNulls<Int>(10).forEach{println(it)}
null
null
null
null
null
null
null
null
null
null

數組Array類,還提供了一個構造函數:

public inline constructor(size: Int, init: (Int) -> T)

第1個參數是數組大小,第2個參數是一個初始化函數類型的參數(關於函數類型,我們將在後面章節介紹)。

代碼示例:

>>> val square = Array(10, { i -> (i*i)})
>>> square
[Ljava.lang.Integer;@6f9e08d4
>>> square.forEach{ println(it) }
0
1
4
9
16
25
36
49
64
81

如上所述,[] 運算符代表調用成員函數 get()set()
代碼示例:

>>> square[3]
9

>>> square[3]=1000
>>> square.forEach{ println(it) }
0
1
4
1000
16
25
36
49
64
81

與 Java 不同的是,Kotlin 中數組不是型變的(invariant)。 Kotlin中,我們不能把 Array<String> 賦值給 Array<Any>。這地方Kotlin類型檢查的限制強於Java的數組類型。

代碼示例:

>>> val arrstr = arrayOf<String>("1","2","3")
>>> arrstr
[Ljava.lang.String;@39374689
>>> var arrany = arrayOf<Any>(Any(),Any(),Any())
>>> arrany
[Ljava.lang.Object;@156324b
>>> arrany = arrstr
error: type mismatch: inferred type is Array<String> but Array<Any> was expected
arrany = arrstr
         ^

原生數組類型

Kotlin 也有無裝箱開銷的專門的類來表示原生類型數組。這些原生數組類如下:

  • BooleanArray
  • ByteArray
  • CharArray
  • ShortArray
  • IntArray
  • LongArray
  • FloatArray
  • DoubleArray
  • BooleanArray

這些類和 Array 並沒有繼承關係,但它們有同樣的函數和屬性集。它們也都有相應的工廠方法:

/**
 * Returns an array containing the specified [Double] numbers.
 */
public fun doubleArrayOf(vararg elements: Double): DoubleArray

/**
 * Returns an array containing the specified [Float] numbers.
 */
public fun floatArrayOf(vararg elements: Float): FloatArray

/**
 * Returns an array containing the specified [Long] numbers.
 */
public fun longArrayOf(vararg elements: Long): LongArray

/**
 * Returns an array containing the specified [Int] numbers.
 */
public fun intArrayOf(vararg elements: Int): IntArray

/**
 * Returns an array containing the specified characters.
 */
public fun charArrayOf(vararg elements: Char): CharArray

/**
 * Returns an array containing the specified [Short] numbers.
 */
public fun shortArrayOf(vararg elements: Short): ShortArray

/**
 * Returns an array containing the specified [Byte] numbers.
 */
public fun byteArrayOf(vararg elements: Byte): ByteArray

/**
 * Returns an array containing the specified boolean values.
 */
public fun booleanArrayOf(vararg elements: Boolean): BooleanArray

代碼示例:

>>> val x: IntArray = intArrayOf(1, 2, 3)
>>> x[0]
1

4.5 Any?可空類型(Nullable Types)

可空類型是Kotlin類型系統的一個特性,主要是爲了解決Java中的令人頭疼的 NullPointerException 問題。

我們知道,在Java中如果一個變量可以是null,來那麼使用它調用一個方法就是不安全的,因爲它會導致:NullPointerException

Kotlin把可空性(nullability)作爲類型系統的一部分,Kotlin編譯器可以直接在編譯過程中發現許多可能的錯誤,並減少在運行時拋出異常的可能性。

Kotlin的類型系統和Java相比,首要的區別就是Kotlin對可空類型的顯式支持。

在本節中,我們將討論Kotlin中的可空類型。

4.5.1 null 是什麼

對於Java程序員來說,null是令人頭痛的東西。我們時常會受到空指針異常(NPE)的騷擾。就連Java的發明者都承認這是他的一項巨大失誤。Java爲什麼要保留null呢?null出現有一段時間了,並且我認爲Java發明者知道null與它解決的問題相比帶來了更多的麻煩,但是null仍然陪伴着Java。

我們通常把null理解爲編程語言中定義特殊的0, 把我們初始化的指針指向它,以防止“野指針”的惡果。在Java中,null是任何引用類型的默認值,不嚴格的說是所有Object類型的默認值。

這裏的null既不是對象也不是一種類型,它僅是一種特殊的值,我們可以將其賦予任何引用類型,也可以將null轉化成任何類型。在編譯和運行時期,將null強制轉換成任何引用類型都是可行的,在運行時期都不會拋出空指針異常。注意,這裏指的是任何Java的引用類型。在遇到基本類型int long float double short byte 等的時候,情況就不一樣了。而且還是個坑。編譯器不會報錯,但是運行時會拋NPE。空指針異常。這是Java中的自動拆箱導致的。代碼示例:

Integer nullInt = null; // this is ok
int anotherInt = nullInt; // 編譯器允許這麼賦值, 但是在運行時拋 NullPointerException

所以,我們寫Java代碼的時候,要時刻注意這一點:Integer的默認值是null而不是0。當把null值傳遞給一個int型變量的時候,Java的自動裝箱將會返回空指針異常。

4.5.2 Kotlin 中的 null

在Kotlin中,針對Java中的null的雜亂局面,進行了整頓,作了清晰的界定,並在編譯器級別強制規範了可空null變量類型的使用。

我們來看一下Kotlin中關於null的一些有趣的運算。

nullnull是相等的:

>>> null==null
true
>>> null!=null
false

null這個值比較特殊,null 不是Any類型

>>> null is Any
false

但是,nullAny?類型:

>>> 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?我們將會在下一小節中介紹。

我們可以對null進行加法運算:

>>> "1"+null
1null

>>> null+20
null20

對應的重載運算符的函數定義在kotlin/Library.kt裏面:

package kotlin

import kotlin.internal.PureReifiable

/**
 * Returns a string representation of the object. Can be called with a null receiver, in which case
 * it returns the string "null".
 */
public fun Any?.toString(): String

/**
 * Concatenates this string with the string representation of the given [other] object. If either the receiver
 * or the [other] object are null, they are represented as the string "null".
 */
public operator fun String?.plus(other: Any?): String

...

但是,反過來就不行了:

>>> 1+null
error: none of the following functions can be called with the arguments supplied: 
public final operator fun plus(other: Byte): Int defined in kotlin.Int
public final operator fun plus(other: Double): Double defined in kotlin.Int
public final operator fun plus(other: Float): Float defined in kotlin.Int
public final operator fun plus(other: Int): Int defined in kotlin.Int
public final operator fun plus(other: Long): Long defined in kotlin.Int
public final operator fun plus(other: Short): Int defined in kotlin.Int
1+null
 ^

這是因爲Int沒有重載傳入null參數的plus()函數。

4.5.3 可空類型 String? 與安全調用 ?.

我們來看一個例子。下面是計算字符串長度的簡單Java方法:

public static int getLength1(String str) {
    return str.length();
}

我們已經習慣了在這樣的Java代碼中,加上這樣的空判斷處理:

public static int getLength2(String str) throws Exception {
    if (null == str) {
        throw new Exception("str is null");
    }

    return str.length();
}

而在Kotlin中,當我們同樣寫一個可能爲null參數的函數時:

fun getLength1(str: String): Int {
    return str.length
}

當我們傳入一個null參數時:

@Test fun testGetLength1() {
    val StringUtilKt = StringUtilKt()
    StringUtilKt.getLength1(null)
}

編譯器就直接編譯失敗:

e: /Users/jack/easykotlin/chapter4_type_system/src/test/kotlin/com/easy/kotlin/StringUtilKtTest.kt: (15, 33): Null can not be a value of a non-null type String
:compileTestKotlin FAILED
FAILURE: Build failed with an exception.
* What went wrong:
Execution failed for task ':compileTestKotlin'.
> Compilation error. See log for more details

如果我們使用IDEA,會在編碼時就直接提示錯誤了:

Kotlin極簡教程

這樣通過編譯時強制排除空指針的錯誤,大大減少了出現NPE的可能。

另外,如果我們確實需要傳入一個可空的參數,我們可以使用可空類型String?來聲明一個可以指向空指針的變量。

可空類型可以用來標記任何一個變量,來表明這個變量是可空的(Nullable)。例如:Char?, Int?, MineType?(自定義的類型)等等。

我們用示例代碼來更加簡潔的說明:

>>> var x:String="x"
>>> x=null
error: null can not be a value of a non-null type String
x=null
  ^

>>> var y:String?="y"
>>> y=null
>>> y
null

我們可以看出:普通String類型,是不允許指向null的;而可空String?類可以指向null

下面我們來嘗試使用一個可空變量來調用函數:

>>> fun getLength2(str: String?): Int? = str.length
error: only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type String?
fun getLength2(str: String?): Int? = str.length
                                        ^

編譯器直接報錯,告訴我們,變量str: String?是可空的類型,調用只能通過安全調用?. 或者 非空斷言調用!!.

另外,如果不需要捕獲異常來處理,我們可以使用Kotlin裏面的安全調用符?.

Kotlin極簡教程

代碼示例:

fun getLength2(str: String?): Int? {
     return str?.length
}

測試代碼:

@Test fun testGetLength2() {
    val StringUtilKt = StringUtilKt()
    println(StringUtilKt.getLength2(null)) //null
    Assert.assertTrue(3 == StringUtilKt.getLength2("abc"))
}

我們可以看出,當我們使用安全調用?. , 代碼安靜的執行輸出了null

如果,我們確實想寫一個出現空指針異常的代碼,那就使用可能出現空指針的斷言調用符!!.

代碼示例:

fun getLength3(str: String?): Int? {
    return str!!.length
}

測試代碼:

@Test fun testGetLength3() {
    val StringUtilKt = StringUtilKt()
    println(StringUtilKt.getLength3(null))
    Assert.assertTrue(3 == StringUtilKt.getLength3("abc"))
}

上面的代碼就跟Java裏面差不多了,運行會直接拋出空指針異常:

kotlin.KotlinNullPointerException
    at com.easy.kotlin.StringUtilKt.getLength3(StringUtilKt.kt:16)
    at com.easy.kotlin.StringUtilKtTest.testGetLength3(StringUtilKtTest.kt:28)

這裏的KotlinNullPointerException 是KotlinNullPointerException.java代碼,繼承了Java中的java.lang.NullPointerException, 它的源代碼如下:

package kotlin;

public class KotlinNullPointerException extends NullPointerException {
    public KotlinNullPointerException() {
    }

    public KotlinNullPointerException(String message) {
        super(message);
    }
}

另外,如果異常需要捕獲到進行特殊處理的場景,在Kotlin中仍然使用 try ... catch 捕獲並處理異常。

4.5.4 可空性的實現原理

我們來看一段Kotlin的可空類型的示例代碼如下:

fun testNullable1(x: String, y: String?): Int {
    return x.length
}

fun testNullable2(x: String, y: String?): Int? {
    return y?.length
}

fun testNullable3(x: String, y: String?): Int? {
    return y!!.length
}

我們來使用IDEA的Kotlin插件來看下可空類型的安全調用的等價Java代碼。

打開IDEA的 Tools > Kotlin > Show Kotlin Bytecode

Kotlin極簡教程

然後,點擊Decompile , 我們可以得到反編譯的Java代碼

public final class NullableTypesKt {
   public static final int testNullable1(@NotNull String x, @Nullable String y) {
      Intrinsics.checkParameterIsNotNull(x, "x");
      return x.length();
   }

   @Nullable
   public static final Integer testNullable2(@NotNull String x, @Nullable String y) {
      Intrinsics.checkParameterIsNotNull(x, "x");
      return y != null?Integer.valueOf(y.length()):null;
   }

   @Nullable
   public static final Integer testNullable3(@NotNull String x, @Nullable String y) {
      Intrinsics.checkParameterIsNotNull(x, "x");
      if(y == null) {
         Intrinsics.throwNpe();
      }

      return Integer.valueOf(y.length());
   }
}

在不可空變量調用函數之前,都檢查了是否爲空, 使用的是kotlin.jvm.internal.Intrinsics這個Java類裏面的checkParameterIsNotNull方法。如果是null就拋出異常:

public static void checkParameterIsNotNull(Object value, String paramName) {
    if (value == null) {
        throwParameterIsNullException(paramName);
    }
}

同時,我們可以看出在Kotlin中函數的入參聲明

fun testNullable(x: String, y: String?) 

反編譯成等價的Java代碼是

public static final void testNullable(@NotNull String x, @Nullable String y) 

我們可以看出,這裏使用註解@NotNull標註不可空的變量,使用註解@Nullable標註一個變量可空。

可空變量的安全調用符y?.length 等價的Java代碼就是:

y != null?Integer.valueOf(y.length()):null

可空變量的斷言調用y!!.length等價的Java代碼是:

if(y == null) {
    Intrinsics.throwNpe();
}
return Integer.valueOf(y.length());

4.5.5 可空類型層次體系

就像Any是在非空類型層次結構的根,
Any?是可空類型層次的根。
由於Any?是Any的超集,所以,Any?是Kotlin的類型層次結構的最頂端。

Kotlin極簡教程

代碼示例:

>>> 1 is Any
true

>>> 1 is Any?
true

>>> null is Any
false

>>> null is Any?
true

>>> Any() is Any?
true

4.6 kotlin.Unit類型

Kotlin也是面向表達式的語言。在Kotlin中所有控制流語句都是表達式(除了變量賦值、異常等)。

Kotlin中的Unit類型實現了與Java中的void一樣的功能。不同的是,當一個函數沒有返回值的時候,我們用Unit來表示這個特徵,而不是null

大多數時候,我們並不需要顯式地返回Unit,或者聲明一個函數的返回類型爲Unit。編譯器會推斷出它。

代碼示例:

>>> fun unitExample(){println("Hello,Unit")}
>>> val helloUnit = unitExample()
Hello,Unit
>>> helloUnit
kotlin.Unit
>>> println(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 {
    }
}

總的來說,這個Unit類型並沒有什麼特別之處。它的源碼是:

package kotlin

/**
 * The type with only one value: the Unit object. This type corresponds to the `void` type in Java.
 */
public object Unit {
    override fun toString() = "kotlin.Unit"
}

跟任何其他類型一樣,它的父類型是Any。如果是一個可空的Unit?,它的父類型是Any?

Kotlin極簡教程

4.7 kotlin.Nothing類型

Kotlin中沒有類似Java和C中的函數沒有返回值的標記void,但是擁有一個對應Nothing。在Java中,返回void的方法,其返回值void是無法被訪問到的:

public class VoidDemo {
    public void voidDemo() {
        System.out.println("Hello,Void");
    }
}

測試代碼:

@org.junit.runner.RunWith(org.junit.runners.JUnit4.class)
public class VoidDemoTest {
    @org.junit.Test
    public void testVoid() {
        VoidDemo voidDemo = new VoidDemo();
        void v = voidDemo.voidDemo(); // 沒有void變量類型,無法訪問到void返回值
        System.out.println(voidDemo.voidDemo()); // error: 'void' type not allowed here
    }
}

在Java中,void不能是變量的類型。也不能被當做值打印輸出。但是,在Java中有個包裝類Voidvoid 的自動裝箱類型。如果你想讓一個方法返回類型 永遠是 null 的話, 可以把返回類型置爲這個大寫的V的Void類型。

代碼示例:

public Void voidDemo() {
    System.out.println("Hello,Void");
    return null;
}

測試代碼:

@org.junit.runner.RunWith(org.junit.runners.JUnit4.class)
public class VoidDemoTest {
    @org.junit.Test
    public void testVoid() {
        VoidDemo voidDemo = new VoidDemo();
        Void v = voidDemo.voidDemo(); // Hello,Void
        System.out.println(v); // null
    }
}

這個Void就是Kotlin中的Nothing?。它的唯一可被訪問到的返回值也是null

在Kotlin類型層次結構的最底層就是類型Nothing

Kotlin極簡教程

正如它的名字Nothing所暗示的,Nothing是沒有實例的類型。

代碼示例:

>>> Nothing() is Any
error: cannot access '<init>': it is private in 'Nothing'
Nothing() is Any
^

注意:Unit與Nothing之間的區別: Unit類型表達式計算結果的返回類型是Unit。Nothing類型的表達式計算結果是永遠不會返回的(跟Java中的void相同)。

例如,throw關鍵字中斷的表達式的計算,並拋出堆棧的功能。所以,一個throw Exception 的代碼就是返回Nothing的表達式。代碼示例:

fun formatCell(value: Double): String =
    if (value.isNaN()) 
        throw IllegalArgumentException("$value is not a number")  // Nothing
    else 
        value.toString()

再例如, Kotlin的標準庫裏面的exitProcess函數:

@file:kotlin.jvm.JvmName("ProcessKt")
@file:kotlin.jvm.JvmVersion
package kotlin.system

/**
 * Terminates the currently running Java Virtual Machine. The
 * argument serves as a status code; by convention, a nonzero status
 * code indicates abnormal termination.
 *
 * This method never returns normally.
 */
@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.")
}

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,被用作任何可空類型的空引用。

Kotlin極簡教程

綜上所述,我們可以看出Kotlin有一個簡單而一致的類型系統。Any?是整個類型體系的頂部,Nothing是底部。如下圖所示:

Kotlin極簡教程

4.8 類型檢測與類型轉換

4.8.1 is,!is運算符

is運算符可以檢查對象是否與特定的類型兼容(“兼容”的意思是:此對象是該類型,或者派生於該類型)。

is運算符用來檢查對象(變量)是否屬於某數據類型(如Int、String、Boolean等)。C#裏面也有這個運算符。

is運算符類似Java的instanceof:

@org.junit.runner.RunWith(org.junit.runners.JUnit4.class)
public class TypeSystemDemo {
    @org.junit.Test
    public void testVoid() {
        if ("abc" instanceof String) {
            println("abc is instanceof String");
        } else {
            println("abc is not instanceof String");
        }
    }

    void println(Object obj) {
        System.out.println(obj);
    }
}

在Kotlin中,我們可以在運行時通過使用 is 操作符或其否定形式 !is 來檢查對象是否符合給定類型:

>>> "abc" is String
true
>>> "abc" !is String
false

>>> null is Any
false
>>> null is Any?
true

代碼示例:

@RunWith(JUnit4::class)
class ASOperatorTest {
    @Test fun testAS() {
        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()

類型自動轉換

在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
}

4.8.2 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語言中最重要的部分之一的:類型系統。

與Java相比,Kotlin的類型系統更加簡單一致,同時引入了一些新的特性,這些特性對於提高代碼的安全性、可靠性至關重要。例如:可空類型和只讀集合。關於只讀集合類,我們將在下一章中介紹。

我們下一章的主題是:Kotlin的集合類和泛型。

本章示例代碼工程:https://github.com/EasyKotlin/chapter4_type_system

參考資料

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章