本文收錄于 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):
- m2的入?yún)⒈患由狭丝蔀閚ull的注解,如下所示:
@Lorg/jetbrains/annotations/Nullable;() // invisible, parameter 0
- 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):
- m3方法的入?yún)⑼瑯颖粯?biāo)注為了可為null,如下所示:
@Lorg/jetbrains/annotations/Nullable;() // invisible, parameter 0
- 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é)下:
- 對于入?yún)⒉豢赡転榭盏念愋?,kotlin編譯器會加上 @Lorg/jetbrains/annotations/NotNull;注解,反之會加上@Lorg/jetbrains/annotations/Nullable;注解。
- 對于不可能為null的入?yún)ⅲ瑒t會直接執(zhí)行對應(yīng)的代碼邏輯。而對于可能為null的入?yún)?,則會根據(jù)調(diào)用方式的不同而不同,參見下面第3點(diǎn)。
- 對于使用 ?. 操作符的語句,kotlin會進(jìn)行調(diào)用變量是否為null的判斷,如果不為null,就執(zhí)行對應(yīng)的代碼邏輯,否則什么都不做;而對于使用 !! 操作符的語句,kotlin同樣也會進(jìn)行是否為null的判斷,只不過當(dāng)調(diào)用變量為null的時(shí)候,會直接拋出空指針異常。
至此,kotlin空安全的場景及其背后的原理分析完畢。