背景
在 Java 語境下桐玻,使用對象總是讓我感到明顯的不安全感简识,這個對象要判空嗎拔妥?這個對象肯定不會為空忿危,不用加判斷了吧?經(jīng)過血淋淋的事實之后没龙,在使用對象之前我總會加上判空處理铺厨,如果調(diào)用的層級有點深,代碼就顯得“惡臭”了硬纤。
而 Kotlin 提供了嚴格的可為 null 規(guī)則解滓,旨在從我們的代碼中消除 NullPointerException,默認情況下筝家,對象的引用不能包含 null 值洼裤。
使用
安全使用
默認情況下,我們創(chuàng)建的所有變量都是不允許為空的溪王,必須給其指定一個值腮鞍,如果給它賦值為 null值骇,就會報錯。如下:
class NullTest {
var str: String = null//出錯移国,默認情況下不能為空
var name:String ="tandeneck"
fun assignNull(){
name = null //出錯吱瘩,不能賦值為空
}
}
當然,以上是默認情況下迹缀,某些情況下我們允許允許為空的變量使碾,那么這時候就需要 的加持變?yōu)榭煽疹愋汀H缦拢?/p>
var name:String? = null
但是由此會帶來空指針異常祝懂,所以在 Android Studio 如果直接調(diào)用的時候 IDE 會報錯:
class User {
var name:String = "tandeneck"
}
fun main() {
var user:User? = null
println(user.name)
}
//報錯信息:Only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type User?
那我們做下判空處理會怎樣票摇?這就要分情況討論了:
情況一:
class MainActivity : AppCompatActivity() {
var textView: TextView? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
private fun test() {
if (textView != null) {
textView.textSize = 20f
//上面一行代碼報錯:
//Smart cast to 'TextView' is impossible,
// because 'textView' is a mutable property
// that could have been changed by this time
}
}
}
根據(jù)報錯信息得知是由于 textView 是可變的,在調(diào)用的時候有可能它已經(jīng)變?yōu)榭樟搜馀睿驗樵诙嗑€程情況下兄朋,其他線程是有可能把它變?yōu)榭盏摹?/p>
那啥,我們把它改為不可變的不就行了嗎怜械?即把 var 改為 val颅和,如下:
val textView: TextView? = null
這樣報錯是不會報錯了,但是沒有意義缕允,因為 textView 不能被重新賦值峡扩,永遠是空的。
情況二:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
private fun test() {
var textView: TextView? = null
if (textView != null) {
textView.textSize = 20f
//上面一行代碼不會報錯
}
}
}
不會報錯的原因是 textView 是一個局部變量障本,保證了調(diào)用時不會有另一個線程改變它的值教届。
其實,Kotlin 提供了很方便的機制驾霜,
textView?.textSize = 20f
這個寫法同樣會對變量做一次非空確認之后再調(diào)用方法案训,并且它可以做到線程安全,這種寫法叫做 Safe Call粪糙。
操作符
除此之外强霎,還有一種雙感嘆號 !! 的用法:
textView!!.textSize = 20f
這種寫法叫做 non-null asserted call,即非空斷言蓉冈,如果為空的情況則會拋出異常城舞,因此慎用。
Elvis 操作符寞酿,()
Elvis 操作符能夠大大簡化 if-else 表達式家夺,如下:
fun main() {
var b: String? = "length"http://定義了一個可能為null的字符串變量str
val length1: Int = if (b != null) b.length else 0
val length2: Int = b?.length ?: 0
}
安全類型轉(zhuǎn)換,
Kotlin 可以使用 as 關(guān)鍵字來進行類型轉(zhuǎn)換,如果對象不是目標類型伐弹,那么類型轉(zhuǎn)換可能會導(dǎo)致 ClassCastException拉馋。這時哦我們選擇 as? ,如果嘗試轉(zhuǎn)換不成功則會返回 null:
fun main() {
var str = "string"
val num: Int? = str as? Int
println(num)
//輸出 null
}
原理
了解空安全的使用之后,下面讓我們來看看其背后的原理,做到知其所以然煌茴。以下面的代碼為例:
fun test1(str: String) = str.toUpperCase()
fun test2(str: String?) = str?.toUpperCase()
fun test3(str: String?) = str!!.toUpperCase()
然后我們查看它們對應(yīng)的字節(jié)碼柠逞,操作方法:Tools -> Kotlin -> Show Kotlin Bytecode,然后點擊 Decompile 反編譯字節(jié)碼得到以下代碼:
public final class TestKt {
@NotNull
public static final String test1(@NotNull String str) {
Intrinsics.checkParameterIsNotNull(str, "str");
boolean var2 = false;
String var10000 = str.toUpperCase();
Intrinsics.checkExpressionValueIsNotNull(var10000, "(this as java.lang.String).toUpperCase()");
return var10000;
}
@Nullable
public static final String test2(@Nullable String str) {
String var10000;
if (str != null) {
boolean var2 = false;
if (str == null) {
throw new TypeCastException("null cannot be cast to non-null type java.lang.String");
}
var10000 = str.toUpperCase();
Intrinsics.checkExpressionValueIsNotNull(var10000, "(this as java.lang.String).toUpperCase()");
} else {
var10000 = null;
}
return var10000;
}
@NotNull
public static final String test3(@Nullable String str) {
if (str == null) {
Intrinsics.throwNpe();
}
boolean var2 = false;
if (str == null) {
throw new TypeCastException("null cannot be cast to non-null type java.lang.String");
} else {
String var10000 = str.toUpperCase();
Intrinsics.checkExpressionValueIsNotNull(var10000, "(this as java.lang.String).toUpperCase()");
return var10000;
}
}
}
我們先看 test1 方法:
首先給參數(shù) str 加上 @NotNull 注解
然后調(diào)用 Intrinsics.checkParameterIsNotNull(str, "str") 方法景馁,其實現(xiàn)如下:
public static void checkParameterIsNotNull(Object value, String paramName) {
if (value == null) {
throwParameterIsNullException(paramName);
}
}
private static void throwParameterIsNullException(String paramName) {
StackTraceElement[] stackTraceElements = Thread.currentThread().getStackTrace();
// #0 Thread.getStackTrace()
// #1 Intrinsics.throwParameterIsNullException
// #2 Intrinsics.checkParameterIsNotNull
// #3 our caller
StackTraceElement caller = stackTraceElements[3];
String className = caller.getClassName();
String methodName = caller.getMethodName();
IllegalArgumentException exception =
new IllegalArgumentException("Parameter specified as non-null is null: " +
"method " + className + "." + methodName +
", parameter " + paramName);
throw sanitizeStackTrace(exception);
}
如果參數(shù)為空,則會拋出異常逗鸣。
最后調(diào)用 toUpperCase() 方法并返回結(jié)果合住。
test2 方法與 test1 不同的地方是注解變?yōu)?@Nullable,傳入的參數(shù)為 null 情況則會返回 null撒璧,否則調(diào)用相應(yīng)的方法透葛。
test3 方法判斷參數(shù)為空時會直接拋出空指針異常,否則調(diào)用相應(yīng)的邏輯卿樱。
由此僚害,我們知道 Kotlin 空安全背后的原理:
- 1.非空類型的屬性編譯器添加@NotNull注解,可空類型添加@Nullable注解繁调;
- 2.非空類型直接對參數(shù)進行判空萨蚕,如果為空直接拋出異常;
- 3.可空類型蹄胰,如果是?.判空岳遥,不空才執(zhí)行后續(xù)代碼,否則返回null裕寨;如果是!!浩蓉,空的話直接拋出NPE異常。
注意事項
Kotlin 并不是絕對的空安全宾袜,以下情況不做特殊處理可能會拋出空指針異常:
- 使用前面提到的 !! 操作符捻艳,
- 與 Java 互操作,如下:
public class User {
public Student student;
public static final class Student {
public String name;
}
}
fun main() {
fun printStudentName(user: User) {
println(user.student.name)
}
printStudentName(User())
//報空指針異常
}
解決的方法也比較簡單:
fun printStudentName(user: User) {
println(user.student?.name)
//這樣就輸出 null庆猫,而不是報異常了
}
- Gson 解析认轨,具體可以參考Android避坑指南,發(fā)現(xiàn)了一個極度不安全的操作月培。
總結(jié)
Kotlin 空安全能幫助我們編寫高效安全的代碼好渠,了解它背后的原理能使我們運用得更加順手。同時节视,也要注意一些坑拳锚,保證代碼的穩(wěn)健性。