kotlin入門潛修之特性及其原理篇—空安全

本文收錄于 kotlin入門潛修專題系列,歡迎學(xué)習(xí)交流。

創(chuàng)作不易,如有轉(zhuǎn)載,還請備注。

空安全

本篇文章將對kotlin中的空安全相關(guān)的知識進(jìn)行闡述,并分析其背后的原理。
kotlin最為人熟知的便是解決了空指針問題,那么kotlin是怎么解決空指針問題的?是否能夠完全避免空指針問題?這就是本節(jié)要闡述的話題。

什么是空指針想必大家都很清楚,這里就不再展開。直接奔入主題:在kotlin中如何實(shí)現(xiàn)空指針安全的?主要體現(xiàn)以下幾個(gè)方面:

第一,kotlin在定義變量的時(shí)候就可以限制該變量是否能為null。
來看個(gè)例子,如下所示:

fun main(args: Array<String>) {
    var str: String = "test"http://定義了類型為String的變量str
    str = null//!!!錯(cuò)誤,str不能為null

    var str2: String? = "test"http://定義了類型為String?的str2
    str2 = null//正確,str2可以為null
}

由代碼來看,kotlin只有在用具體類型加上?(上面代碼中的String?)來修飾變量的時(shí)候,才允許該變量為null。因此,如果我們不期望某個(gè)變量或者某個(gè)入?yún)閚ull的時(shí)候,直接使用具體類型修飾即可,此時(shí)無法接收可能為null的入?yún)?;而?dāng)我們可以接受某個(gè)變量或者入?yún)閚ull時(shí),就可以使用 具體類型加 ? 來修飾,如下所示:

//方法m1,不能接受為null的字符串類型入?yún)?fun m1(p1: String) {}
//方法m2,可以接受為null的字符串類型入?yún)?fun m2(p2: String?) {}

fun main(args: Array<String>) {
    var str2: String? = "test"
    m1(str2)//!!!錯(cuò)誤,無法接受可能為null的入?yún)?    m2(str2)//正確,可以接受為null的入?yún)?}    

第二,kotlin了提供了安全調(diào)用操作符 (?.) ,示例如下:

fun main(args: Array<String>) {
    var str: String? = null
    println(str.length)//!!!編譯錯(cuò)誤,對于可能為null的類型,不能直接這么使用
    println(str?.length)//正確,打印null
    str = "test"
    println(str?.length)//正確,打印 4
}

這就是kotlin的安全調(diào)用方式,即對于可能為null的類型,必須要使用安全的調(diào)用方式。咋一看,這種方式像是變量名后面跟了個(gè)問號,但是實(shí)際上卻不是這樣的,這個(gè)?不是和變量綁定的,而是和點(diǎn)(.)綁定的,即 ?. 是一個(gè)操作符,可以實(shí)現(xiàn)安全調(diào)用。這種調(diào)用方式在變量為null的時(shí)候不會crash而是打印null,在變量不為null的時(shí)候則正常執(zhí)行代碼。

第三,kotlin提供了 !! 操作符,當(dāng)對象為null時(shí),會強(qiáng)制拋出異常。示例如下:

fun main(args: Array<String>) {
    var str: String? = null
    println(str?.length)//正確,可以通過安全調(diào)用操作符調(diào)用
    println(str!!.length)//編譯正確,但是因?yàn)閟tr此時(shí)為null,故會拋出空指針異常。
    str = "test"
    println(str!!.length)//正確,打印 4,因?yàn)閟tr不為null
}

上面代碼在變量不為null的時(shí)候會正確執(zhí)行,但是當(dāng)變量為null的時(shí)候則會拋出kotlin.KotlinNullPointerException空指針異常。

其實(shí),從效果上來看 !! 操作符并不是為了解決空安全問題的,因?yàn)槠鋾伋隹罩羔槷惓?,這個(gè)只是kotlin提供的另一種關(guān)于空處理的方式而已。

第四,一定條件下,kotlin擁有智能推斷變量是否為null的能力,如下所示:

fun main(args: Array<String>) {
    var str: String? = null
    println(str.length)//!!!錯(cuò)誤,str可能為null,無法直接使用,可以通過str?.length來調(diào)用
    var result = if (str != null) str.length else 0//正確!這里竟然又可以通過str.length來完成調(diào)用了??
    println(result)
}

重點(diǎn)關(guān)注 if (str != null) str.length else 0這一句,按照常理,對于可能為null的變量,必須通過?.操作符或者!!操作符調(diào)用,才能編譯通過。然而,此處我們竟然沒有通過這兩種操作符,同樣完成了調(diào)用!這是為什么?

這是因?yàn)?,我們已?jīng)在前面通過if else語句進(jìn)行了判斷,所以kotlin可以據(jù)此智能推斷出,在執(zhí)行str.length的時(shí)候,str已不可能為null,所以允許這么寫。

上面幾條就是kotlin在空安全方面所做的工作,這將大大減輕我們寫程序的壓力,尤其是安全調(diào)用操作符,不僅能夠有效減免空指針問題,還能大大減少代碼量。

kotlin空安全中的"不安全性"

看這個(gè)標(biāo)題實(shí)在有點(diǎn)難以明白是什么意思,其實(shí)這個(gè)小節(jié)想要表達(dá)的意思就是,即使kotlin在空安全方面做了很多的工作,但是依然無法完全避免空指針的產(chǎn)生,來看個(gè)例子。

//注意,這個(gè)是個(gè)java代碼
public class Test {
//定義了一個(gè)靜態(tài)方法getStr,這里直接返回null
    public static String getStr(){
        return null;
    }
}

上面代碼是java代碼,我們定義了一個(gè)getStr的靜態(tài)方法,該方法直接返回了null,下面我們通過kotlin代碼來使用getStr方法,如下所示:

import test.Test
fun main(args: Array<String>) {
    println(Test.getStr().length)
}

上面代碼執(zhí)行完成之后會發(fā)生什么?顯然會產(chǎn)生空指針異常:java.lang.NullPointerException。這就是為什么說kotlin并不能完全避免空指針異常的問題。

如果說上面的例子是因?yàn)檎{(diào)用了java代碼才產(chǎn)生了空指針異常,那么現(xiàn)在來看一個(gè)單純使用kotlin也會產(chǎn)生空指針異常的場景,示例如下:

//定義了一個(gè)抽象類Test
abstract class Test {
//定義 了一個(gè)抽象屬性str,子類復(fù)寫了該屬性
    abstract var str: String
    constructor() {
//然后我們在父類中的構(gòu)造方法中打印str 的長度
        println(str.length)
    }
}
//這里定義了子類SubTest,復(fù)寫了Test的str屬性,并完成了賦值
class SubTest : Test() {
    override var str: String = ""
}
//測試方法main
fun main(args: Array<String>) {
    SubTest()//僅僅生成了一個(gè)子類對象
}

上述代碼執(zhí)行完后會產(chǎn)生什么問題?答案是會產(chǎn)生空指針異常!這是為什么?我們來分析下:

在執(zhí)行SubTest()語句的時(shí)候,kotlin會首先執(zhí)行父類的構(gòu)造方法,然后再去完成子類屬性的初始化,也就是說父類構(gòu)造方法的初始化時(shí)機(jī)要高于子類屬性的初始化時(shí)機(jī)。所以,在父類構(gòu)造方法中打印str的長度的時(shí)候,實(shí)際上子類屬性還沒有完成初始化,進(jìn)而產(chǎn)生了空指針異常。實(shí)際上如果我們在生成對象之后在打印str的長度,就不會產(chǎn)生空指針異常,因?yàn)榇藭r(shí)str已經(jīng)完成了初始化,如下所示:

abstract class Test {
    abstract open var str: String
    constructor() {
    }
    fun m1(){
        str.length
    }
}
class SubTest : Test() {
    override var str: String = ""
}
fun main(args: Array<String>) {
    SubTest().m1()//這里會正常執(zhí)行,打印0,因?yàn)閟tr為"",所以其長度為0
}

安全類型轉(zhuǎn)換

在java編程的時(shí)候,我們一定會遇到過ClassCastException這個(gè)異常,即類型轉(zhuǎn)換異常,比如我們將String轉(zhuǎn)換為Integer,這個(gè)就會引起類型轉(zhuǎn)換異常,那么在kotlin中,我們可以避免這種情況的發(fā)生,那就是通過使用安全類型轉(zhuǎn)換as?來完成,使用as?進(jìn)行類型轉(zhuǎn)換的時(shí)候,如果轉(zhuǎn)換失敗則會將目標(biāo)賦值為null,如下所示:

fun main(args: Array<String>) {
    var str: String? = null
    var value: Int = str as Int//拋出kotlin.TypeCastException異常
    var value2: Int? = str as? Int//能正確運(yùn)行,只不過value2為null
    System.out.println(value2)//打印null
}

Elvis 操作符

Elvis 操作符能夠大大簡化if else表達(dá)式,可以省去繁瑣的null判斷,如下所示:

fun main(args: Array<String>) {
    var str: String? = "test"http://定義了一個(gè)可能為null的字符串變量str
//我們可以通過if表達(dá)式來獲取str的長度,但是比較麻煩
    val value: Int = if (str != null) str.length else 0
//這里我們通過Elvis操作符來獲取str的長度,顯得非常簡潔
    val value2: Int = str?.length ?: 0
}

上面代碼中的 ?: 操作符就是Elvis操作符,可以大大簡化代碼量。

空安全背后的原理

前面闡述了kotlin中關(guān)于空安全的幾種場景,下面我們看下其背后的原理,照例先上我們要分析的代碼:

//場景1,m1方法接收一個(gè)不可能為null的字符串
//在其方法體中我們獲取了傳入字符串的長度
fun m1(str: String) {
    str.length
}
//場景2,m2方法接收一個(gè)可能為null的字符串
//在其方法體中我們采用了安全調(diào)用操作符 ?. 來獲取傳入字符串的長度
fun m2(str: String?) {
    str?.length
}
//場景3,m3方法接收一個(gè)可能為null的字符串
//在其方法體中我們采用了 !!  來獲取傳入字符串的長度
fun m3(str: String?) {
    str!!.length
}

那么上面三種場景,kotlin都是怎么處理的呢?這里一個(gè)一個(gè)的來分析下。

首先看下場景1背后的字節(jié)碼,如下所示:

  public final static m1(Ljava/lang/String;)V
    @Lorg/jetbrains/annotations/NotNull;() // invisible, parameter 0
   L0
    ALOAD 0
    LDC "str"
    INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkParameterIsNotNull (Ljava/lang/Object;Ljava/lang/String;)V
   L1
    LINENUMBER 6 L1
    ALOAD 0
    INVOKEVIRTUAL java/lang/String.length ()I
    POP
   L2
    LINENUMBER 7 L2
    RETURN
   L3
    LOCALVARIABLE str Ljava/lang/String; L0 L3 0
    MAXSTACK = 2
    MAXLOCALS = 1

由字節(jié)碼可知,該方法的入?yún)患由戏强兆⒔?,如下所示?/p>

    @Lorg/jetbrains/annotations/NotNull;() // invisible, parameter 0

之后,kotlin編譯器內(nèi)部調(diào)用了是否為null的檢查,這就是為什么我們傳入null的時(shí)候會編譯報(bào)錯(cuò),如下所示:

    INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkParameterIsNotNull (Ljava/lang/Object;Ljava/lang/String;)V

接著會直接調(diào)用str的length方法,返回str長度,如下所示:

    INVOKEVIRTUAL java/lang/String.length ()I

上面就是m1方法背后的原理,下面來看下m2方法背后的原理,如下所示:

// access flags 0x19
  public final static m2(Ljava/lang/String;)V
    @Lorg/jetbrains/annotations/Nullable;() // invisible, parameter 0
   L0
    LINENUMBER 10 L0
    ALOAD 0
    DUP
    IFNULL L1
    INVOKEVIRTUAL java/lang/String.length ()I
    POP
    GOTO L2
   L1
    POP
   L2
   L3
    LINENUMBER 11 L3
    RETURN
   L4
    LOCALVARIABLE str Ljava/lang/String; L0 L4 0
    MAXSTACK = 2
    MAXLOCALS = 1

m2方法需要關(guān)注以下幾個(gè)點(diǎn):

  1. m2的入?yún)⒈患由狭丝蔀閚ull的注解,如下所示:
    @Lorg/jetbrains/annotations/Nullable;() // invisible, parameter 0
  1. kotlin編譯器對該場景做了如下處理:如果為null則什么都不做,否則直接調(diào)用str的length方法,如下所示:
    IFNULL L1//如果為null,則執(zhí)行L1,即直接出棧
    INVOKEVIRTUAL java/lang/String.length ()I//否則調(diào)用str的length方法
    POP
    GOTO L2
   L1
    POP

最后,再來看下m3方法對應(yīng)的字節(jié)碼,如下所示:

 public final static m3(Ljava/lang/String;)V
    @Lorg/jetbrains/annotations/Nullable;() // invisible, parameter 0
   L0
    LINENUMBER 15 L0
    ALOAD 0
    DUP
    IFNONNULL L1
    INVOKESTATIC kotlin/jvm/internal/Intrinsics.throwNpe ()V
   L1
    INVOKEVIRTUAL java/lang/String.length ()I
    POP
   L2
    LINENUMBER 16 L2
    RETURN
   L3
    LOCALVARIABLE str Ljava/lang/String; L0 L3 0
    MAXSTACK = 3
    MAXLOCALS = 1

對于m3方法來說也只需要關(guān)注以下幾個(gè)點(diǎn):

  1. m3方法的入?yún)⑼瑯颖粯?biāo)注為了可為null,如下所示:
    @Lorg/jetbrains/annotations/Nullable;() // invisible, parameter 0
  1. m3方法對于傳入為null的字符串直接拋出空指針異常,否則調(diào)用其length方法,如下所示:
//如果入?yún)⒉粸閚ull,則執(zhí)行L1,即調(diào)用str的length方法
    IFNONNULL L1
//否則,kotlin會直接拋出空指針異常,即調(diào)用Intrinsics.throwNpe ()
    INVOKESTATIC kotlin/jvm/internal/Intrinsics.throwNpe ()V
   L1
    INVOKEVIRTUAL java/lang/String.length ()I
    POP

上面三種場景都分析完了,現(xiàn)在我們來總結(jié)下:

  1. 對于入?yún)⒉豢赡転榭盏念愋?,kotlin編譯器會加上 @Lorg/jetbrains/annotations/NotNull;注解,反之會加上@Lorg/jetbrains/annotations/Nullable;注解。
  2. 對于不可能為null的入?yún)ⅲ瑒t會直接執(zhí)行對應(yīng)的代碼邏輯。而對于可能為null的入?yún)?,則會根據(jù)調(diào)用方式的不同而不同,參見下面第3點(diǎn)。
  3. 對于使用 ?. 操作符的語句,kotlin會進(jìn)行調(diào)用變量是否為null的判斷,如果不為null,就執(zhí)行對應(yīng)的代碼邏輯,否則什么都不做;而對于使用 !! 操作符的語句,kotlin同樣也會進(jìn)行是否為null的判斷,只不過當(dāng)調(diào)用變量為null的時(shí)候,會直接拋出空指針異常。

至此,kotlin空安全的場景及其背后的原理分析完畢。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容