Kotlin系列之可空類型的處理

在前面的文章中,咱們已經看到了kotlin爲了解決NPE問題做出的一些努力。這篇文章咱們繼續學習kotlin中與可空類型處理相關的一些知識。java

非空斷言

在程序的編寫過程當中有這樣一種場景,咱們已經在前一個函數中對一個可空類型的變量進行了檢查,以後咱們在接下來的函數中使用這個變量,咱們其實已經很明確地知道這個變量前面已經進行了判空處理,後續不可能爲空,可是編譯器沒法清楚地推測出來,這時候在編譯器眼裏這個變量仍是有可能爲空,這就形成咱們的代碼中出現了不少無用的判空代碼。安全

這種狀況下咱們就可使用非斷言「!!」。咱們在一個可空的變量後面加上兩個感嘆號,用來明確告訴編譯器咱們肯定這個變量是非空的。這樣咱們就簡單粗暴地實現了將一個可空的變量轉換爲一個非空變量。bash

以下面的代碼所示:ide

fun testNull(str: String?){
    val notNullStr: String = str!!
    println(notNullStr.length)
}
複製代碼

上面這種狀況下至關於咱們強制進行了轉化,因此非空就由咱們本身來保證,一旦傳入了空類型,程序就會拋出異常。好比下面的異常:函數

Exception in thread "main" kotlin.KotlinNullPointerException
	at MainKt.testNull(Main.kt:6)
	at MainKt.main(Main.kt:2)
複製代碼

let函數

有時候,咱們會有這樣的需求,咱們有一個可空類型的變量,可是咱們要將其傳入一個要求是非空參數的函數中,那咱們必須在傳遞以前作非空判斷,不然編譯器不容許咱們直接傳入,就像下面這樣:學習

fun sendMsg(msg: String){
    //xxxxx
}

fun main(args: Array<String>) {
    val msg: String? = "something"
    
    if (msg != null) sendMsg(msg)
}
複製代碼

針對上面這種很常見的狀況,kotlin爲咱們提供了一個let函數,這個函數能夠將調用它的參數轉化爲lambda表達式中的參數。就像下面這樣:ui

msg.let {
    println(it)
}
複製代碼

上面lambda表達式中的it其實就是msg變量,結合上一節介紹過的安全調用運算符,咱們能夠這樣寫:this

msg?.let{
    println(it.length)
}
複製代碼

上面的let函數在msg爲空時不會發生調用,當msg不爲空時,傳遞到let函數的lambda表達式中的it變量天然就變成了非空變量,這樣就完美且優雅地將一個可空類型的變量轉化爲了非空類型的變量。spa

延遲初始化屬性

在kotlin中,你必須在構造函數中初始化全部非空類型的屬性,不然就會報錯,就像下面這樣:code

class Main{
    private var mText: String
    
    constructor(){
        mText = ""
    }

    fun showLen(){
        println(mText.length)
    }
}
複製代碼

可是有時候咱們但願將屬性的初始化放到本身的初始化函數中去完成,那這樣咱們就與kotlin中的規定衝突了,那咱們就必須將屬性聲明爲可空屬性,可是那樣,又致使咱們在調用這個屬性的方法時必須使用安全調用符或者是「!!」將可空類型轉化爲非空類型,那樣的代碼都不夠簡潔優雅,就像下面這樣:

class Main{
    private var mText: String? = null

    fun initArgs(){
        mText = "xxxxx"
    }
    
    fun showLen(){
        println(mText?.length)
    }
}
複製代碼

對於這種咱們想本身掌握屬性的初始化,同時又想將屬性聲明爲非空類型的狀況,kotlin提供了延遲初始化屬性,使用「lateinit」修飾符來表示一個延遲初始化的屬性,擁有這個修飾符的屬性,kotlin不會強制你必須在構造函數中初始化屬性,你能夠本身在任意的時刻本身掌握屬性的初始化。就像下面這樣:

class Main{
    private lateinit var mText: String

    fun initArgs(){
        mText = "xxxxx"
    }

    fun showLen(){
        println(mText.length)
    }
}
複製代碼

這樣的代碼就優雅了許多。假如咱們上面屬性的初始化時機不對,致使咱們還沒初始化這個屬性就已經調用了這個屬性的一些方法,就會報下面的這個異常:

Exception in thread "main" kotlin.UninitializedPropertyAccessException: 
lateinit property mText has not been initialized
複製代碼

異常也對這種狀況說明得很是清楚,延遲初始化屬性還沒初始化便進行了訪問。

可空類型的擴展函數

在Java中,調用一個對象的方法,若是這個對象爲null,就會發生NPE,而後kotlin中經過安全調用符來解決這個問題,保證了在變量不爲null時調用纔會發生。可是有時候咱們就須要一些函數能夠包括對null的檢查,不須要咱們在函數外部進行檢查。 咱們先看下面的這段Java代碼:

public boolean isEmpty(String str){
    if (str == null){
        return true;
    }

    return str.isEmpty();
}
複製代碼

咱們自定義的一個函數,在調用isEmpty()以前,咱們必須本身先判空,處理掉爲null這種狀況,不然就可能出現NPE。咱們想能夠不能夠定義一種擴展函數,它能夠被null進行調用,而且不會報NPE,這就是kotlin中的可空類型的擴展函數。

注意:kotlin中只有擴展函數是能夠針對可空類型的,常規的方法使用null去調用,要麼是編譯失敗,要麼就是NPE,咱們下面舉個例子,看下面的代碼:

fun String?.isEmpty(): Boolean = this == null || this.isBlank()
複製代碼

這樣一行代碼,咱們就爲String的可空類型定義了一個isEmpty()這樣一個擴展函數,也就是說null也能夠調用這個函數。值得注意的是,函數中的this可能爲null,第一個this進行了null的處理後,第二個this就成了一個非空類型的變量。而後咱們就能夠想下面這樣進行調用了:

val nullStr: String? = null
println(nullStr.isEmpty())
複製代碼

注意,上面的調用咱們並無使用安全調用運算符,而且代碼也沒有報NPE,由於它是爲String的可空類型的定義的擴展函數,在函數內部包含了對null值的處理。

可空性與Java的統一

雖然kotlin中有可空類型和非空類型,可是在Java中卻不是這樣劃分的,可是,咱們會常常進行Java和kotlin的互調。那在互調時這種狀況是怎麼處理的呢?

存在註解的狀況

在Java或者是Android中都提供了這樣兩種註解「@NotNull」和「@Nullable」,一種表示變量不可爲空,一種表示變量可爲空,這主要是爲了輔助編譯器檢查代碼,就像下面這樣:

public void showMsg(@NotNull String tag, @Nullable String msg){
        
}
複製代碼

當咱們發生Java代碼和kotlin代碼的互相調用時,kotlin會將使用@Nullable註解的變量對應到kotlin中的可空類型的變量,將使用@NotNull註解的變量對應到kotlin中的非空類型的變量。

不存在註解的狀況

在咱們平常使用的大部分類庫中,其實都沒有上面介紹的兩種註解,那kotlin是怎麼處理的呢?kotlin針對這種狀況提出了一種類型叫平臺類型,也就是沒有註解的Java類型,對應爲kotlin中的平臺類型。你能夠將其看做是kotlin中的可空類型,也能夠看做是kotlin中的非空類型,並且你能夠在其上面調用各類方法,編譯器都不會報錯,由於kotlin將變量的可空性控制權交到了使用者手上,徹底由使用者來控制,並且若是你使用null進行了調用,就會報NPE。就像下面這樣。

//咱們在Java中定義的一個Message類
public class Message {
    private String msg;
    private int code;


    public Message(String msg, int code) {
        this.msg = msg;
        this.code = code;
    }

    public String getMsg() {
        return msg;
    }

    public void setMsg(String msg) {
        this.msg = msg;
    }

    public int getCode() {
        return code;
    }

    public void setCode(int code) {
        this.code = code;
    }
}
複製代碼
//在kotlin中進行調用
fun showMsg(msg: Message){
    println(msg.msg.toUpperCase())
}

fun main(args: Array<String>) {
    showMsg(Message(null, 500))
}
複製代碼

kotlin的編譯器根本沒法肯定Message類中msg屬性的可空性,因此只能將其做爲平臺類型由用戶控制,因此上面的代碼運行會出現NPE。

繼承的狀況

當kotlin繼承或者實現了Java中的某個類或者接口時,那對於方法的參數和返回值怎麼處理呢?這裏咱們只討論沒有註解的平臺類型,存在註解的狀況跟前面說過的同樣。咱們看下面的代碼例子:

//Java定義的接口
interface Account{
    void login(String username);
}
複製代碼
//kotlin中實現接口(參數類型能夠是可空類型)
class UserAccount: Account{
    override fun login(username: String?) {

    }
}
複製代碼
//kotlin中實現接口(參數類型能夠是不可空類型)
class UserAccount: Account{
    override fun login(username: String) {

    }
}
複製代碼

上面的兩種狀況,kotlin的編譯器都不會報錯,均可以編譯經過。因此這個徹底由開發者本身掌控。

kotlin中的類型在Java中被調用

前面說的幾種狀況都是Java中的類型在kotlin中的處理狀況,那麼kotlin中的可空類型和不可空類型在Java中是怎麼處理的呢?

//kotlin中聲明一個非空參數的函數
fun showToast(msg: String){
}
複製代碼

在Java中使用以下代碼進行調用:

public static void main(String[] args) {
    MainKt.showToast(null);
}
複製代碼

而後你會發現拋出了一個異常以下:

Exception in thread "main" java.lang.IllegalArgumentException: 
Parameter specified as non-null is null: 
method MainKt.showToast, parameter msg
複製代碼

仔細想一下會發現有個疑問,雖然咱們給showToast函數傳遞了一個null,可是咱們的showToast函數中並無對傳入的參數進行使用啊。在Java中,只有咱們在null對象上發生了調用,纔會報異常,可是在koltin中則不一樣。再仔細查看上面的異常,你會發現它報的並非NPE,而是參數異常。由此得出結論,kotlin中會爲每個聲明爲非空類型的參數生成一個非空斷言,當咱們嘗試在Java中傳入一個null值時,就會觸發這個斷言,也就是咱們上面看到的異常。

寫在最後

本節主要介紹了kotlin中對於可空類型的處理的一些方法和運算符,能夠看出kotlin中仍是有不少特性來保證代碼儘量的優雅。可是在與Java進行互調的過程當中,爲了保證儘量與Java良好的互調特性,因此存在上面介紹的平臺類型,因此仍是無法徹底避免NPE的產生。

相關文章
相關標籤/搜索