kotlin中的類型可空性

做爲一個程序員,最多見的問題恐怕就是NPE了吧,有時候即便很當心的編碼,仍是避免不了出現NPE,在Kotlin中,它力爭把這個在運行時經常出現的問題在編譯器解決掉,讓咱們寫出更加健壯不易崩潰的代碼。java

Java的類型系統存在的問題

在說kotlin以前,咱們先談談Java中的類型系統。什麼是類型呢?通俗來說其實就是對全部咱們要表示的數據肯定一個具體的分類。 好比,咱們把12這個數據分爲int這個分類(或類型),咱們把"ABC"這個數據分爲String分類(或類型)。那爲何要分類呢?實際上是爲了對不一樣的類型作不一樣的運算和處理。好比知道一個數據是int類型,那咱們就能夠對其進行加減乘除等運算,知道一個數據是字符串類型,咱們就能夠對其求長度。 可是,在Java的類型系統中存在這樣的問題,對於全部的引用類型的對象,當它們沒有被初始化時,他們的值都爲null。這裏存在什麼問題呢。看下面這行簡單的Java代碼:程序員

String str = "ABC";
System.out.println(str.length());
str = null;
System.out.println(str.length());
複製代碼

上面的代碼,咱們聲明瞭一個String類型的變量str,而後咱們將"ABC"這個字符串賦值給它,咱們調用length()函數,它能夠正確打印出字符串長度,這是很合理的。但當我給str賦值爲null後,咱們一樣能夠對str變量調用length()函數,可是這時候卻報了NPE。這時候咱們回頭分析一下這段咱們習覺得常的代碼存在的問題,那就是在Java的類型系統中,全部的引用類型均可以容許被賦值爲null,並且Java也容許咱們對null調用length()方法,至少這麼寫是能夠正常編譯的,可是卻在運行時報NPE。爲了不NPE咱們不得不在調用方法前對str變量進行額外的null檢查,這就致使咱們的代碼中不得不出現不少非空檢查的防護性代碼。安全

下面就來看看kotlin的設計者是如何解決上述問題的。ide

kotlin中的可空類型

上面咱們說,Java存在的問題是對於一個引用類型的變量,它容許咱們將引用類型對象和null均可以賦值給這個變量。函數

kotlin的作法也比較簡單,你聲明的變量默認就只能將非空的值賦值給這個變量,一旦你將null賦值給這個變量,你會發現你的代碼連編譯都過不了。這樣就在必定程度上在編譯期提早幫我咱們暴露了問題。 有時候你想聲明一個變量能夠給它賦值爲null,那你就必須顯式聲明它是可空類型,也就代表你很清楚它賦值爲null是沒問題的。ui

下面的代碼給出非空類型的聲明方式。編碼

//str變量爲非空類型
fun len(str: String) = str.length

//傳入null編譯會報錯
fun main(args: Array<String>) {
    len(null)
}
複製代碼

若是你想讓一個變量容許賦值爲null,即將其變爲可空類型,聲明方式爲在類型後面加一個「?」,以下代碼所示:spa

//str變量爲可空類型
fun len(str: String?) = str.length

//傳入null編譯不會報錯
fun main(args: Array<String>) {
    len(null)
}
複製代碼

下面是可空類型和非空類型的幾點特性,都很好理解,結合下面的示例代碼:設計

fun main(args: Array<String>) {
    var str1: String? = null
    //可空類型是不容許直接調用String的方法和屬性的,編譯報錯
    val len = str1.length

    //可空類型變量是不能夠直接賦值給非空類型變量的,編譯報錯
    var str2: String = str1
    
    //非空類型變量是能夠直接賦值給可空類型變量的
    var str3: String = "value"
    str1 = str3
}
複製代碼
  • 可空類型是不容許直接調用對象的方法和屬性的,編譯報錯
  • 可空類型變量是不能夠直接賦值給非空類型變量的,編譯報錯
  • 非空類型變量是能夠直接賦值給可空類型變量的

還有一點要注意,對於一個可空類型,若是你顯式作了null的檢查,那你就能夠直接在可空類型上面調用這個對象的方法和屬性了,以下面的代碼:code

fun len(str: String?): Int = 
    if (str != null) str.length else 0
複製代碼

在沒有判斷null以前,調用str.length,編譯器會報錯,如今判斷完null以後,編譯器就容許咱們調用了。

安全調用運算符

在Java中,咱們常常會寫下面的代碼:

if (str != null){
    return str.toUpperCase();
}else {
    return null;
}
複製代碼

在kotlin中,咱們對於可空類型的變量,也會寫出下面的代碼:

if (str != null) str.toUpperCase() else return null
複製代碼

kotlin中對於這種常見的模版代碼,提供了一種專門的運算符,叫作安全調用運算符,它的具體形式是"?." 這個運算符的運算規則是,若是被調用的變量不是null,則正常調用並返回結果,若是調用的變量等於null,則此次調用不會發生,直接返回null。這就有效避免了NPE問題。 咱們看看上面的例子,在kotlin中使用安全調用運算符該怎麼寫:

return str?.toUpperCase()
複製代碼

對,就是這麼簡單的一行,就把判空操做直接也包含在內了。 注意,安全調用運算符的返回結果是一個可空類型的變量,由於它有可能會返回null。即像下面這樣:

val res: String? = str?.toUpperCase();
複製代碼

上面的例子可能還不夠吸引你,下面的這個例子絕對可讓你喜歡上這個運算符: 咱們定義一個對象的時候,常常會有對象嵌套的狀況,可是當咱們從一個對象裏面訪問嵌套的對象的屬性時,常常須要作不少層的null檢查,就像下面的kotlin代碼同樣:

data class Son(val name: String?, val age: Int?)

data class Person(val name: String?, val age: Int?, val son: Son?)

fun Person.getSonAge(): Int{
    if (son != null){
        val sonAge = son.age
        return if (sonAge != null) sonAge else 0
    }else{
        return 0
    }
}
複製代碼

對於上面的例子,咱們發現每一個變量都有多是null,咱們就必須在訪問它們的屬性時必須加上醜陋的null檢查代碼,不然就有可能形成NPE,當咱們使用安全調用運算符後,咱們就能夠像下面這樣一行調用了:

fun Person.getSonAge(): Int? = son?.age
複製代碼

不管咱們的對象定義的嵌套層級有多深,咱們均可以使用安全調用運算符直接訪問屬性,當其中某一層的屬性爲null時,就直接返回null,而再也不去訪問後面的屬性,使代碼簡潔了不少。

null合併運算符

上面的代碼仍是存在不太優雅的地方。上面的例子,當對象屬性爲空時,咱們但願age能夠返回0,而不是null,在Java中咱們可使用三目運算符,在kotlin中不存在三目運算符,那咱們就必須使用if判斷了,像下面這樣寫:

fun Person.getSonAge() = if (son?.age != null) son.age else 0
複製代碼

可是kotlin中提供了null合併運算符"?:",能夠簡化上面的判斷邏輯,根據名字就知道,它是將咱們對null的判斷進行了合併簡化,注意,這個運算符跟Java的三目運算符很像,但它是個二元運算符。 它的運算規則是,若是第一個運算數不爲null,那結果就是第一個運算數,若是第一個運算數爲null,那結果就是第二個運算數,因此上面的代碼能夠簡化爲下面的樣子:

fun Person.getSonAge() = son?.age ?: 0
複製代碼

是否是簡潔了不少,可是我的感受第一次接觸仍是以爲不利於代碼的理解,熟悉了之後仍是很不錯的。 固然,咱們有時候不想在值爲null的時候返回一個默認值,咱們想拋出一個異常,那也是能夠的,就像下面這樣:

fun Person.getSonAge() = son?.age ?: throw IllegalArgumentException("no age")
複製代碼

安全轉換運算符

在Java中,咱們想驗證一個對象是否是某個類型,是使用instanceof關鍵字來檢查,而後再進行對象的強轉,若是咱們直接強轉就會出現ClassCastException異常。在kotlin中,一樣的功能咱們使用is進行類型檢查,使用as進行類型強轉,就像下面這樣:

class Person(val name: String, val age: Int){
    override fun equals(other: Any?): Boolean {
        if (other is Person){
            //檢查成功後會只能轉化other對象爲Person類型
            // val otherPerson = other as Person
            return other.name == name && other.age == age
        }else{
            return false
        }
    }

    override fun hashCode(): Int = name.hashCode() * 37 + age
}
複製代碼

上面的例子是重寫了Person類的equals和hashCode方法。 kotlin針對上面的狀況,爲咱們提供了安全轉化運算符"as?",簡單來講就是若是一個對象能夠成功轉換爲某個類型,則會自動轉換,不然返回null,這樣咱們再利用null合併運算符進行處理,就能夠寫出很是優雅的代碼,下面的代碼用更優雅的方式實現了上面的功能:

class Person(val name: String, val age: Int){
    override fun equals(other: Any?): Boolean {
        val otherPerson = other as? Person ?: return false;
        return other.name == name && other.age == age
    }

    override fun hashCode(): Int = name.hashCode() * 37 + age
}
複製代碼

是否是很是簡潔,固然仍是上面說的,初次接觸會以爲這個東西對代碼可讀性不太友好,使用熟悉之後就寫起來很是舒服了。

寫在最後

相對於Java,kotlin中對類型系統中的可空性作了改進,並經過上面一系列的特性和運算符儘量減小NPE問題的出現,幫我咱們寫出安全的代碼,同時也不失其簡潔性。

歡迎關注。
相關文章
相關標籤/搜索