本文大纲:
- kotlin空安全字节码原理
- kotlin空安全潜藏问题
- 空安全编程实战建议
- 总结反思kotlin空安全设计
在空安全 - Kotlin语言中文网中介绍了什么是Kotlin空安全,和Kotlin中可能引起NPE的场景。我们本文结合字节码来分析空安全实现原理、潜藏问题、实战建议和空安全总结。
Kotlin空安全字节码原理
结论先行:
Java的类型本身是可以指向空的,Kotlin多了一个非空类型,是由于Kotlin在编译期生成了额外的注解代码和检查代码,才得以支持了Kotlin非空类型。
代码分析:
用以下5个函数来分析:
// 非空参数调用
fun test_1(str: String) = str.length
// 可空参数调用
fun test_2(str: String?) = str?.length
// 可空参数断言
fun test_3(str: String?) = str!!.length
// 可空参数强转非空参数
fun test_4(str: Any?) { str as String }
// 非空参数强转非空参数
fun test_5(str: Any?) { str as? String }
接着我们逐个分析,每个Kotlin函数都附上对应反编译的Java方法。
非空参数调用
// 非空参数调用
fun test_1(str: String) = str.length
public static final int test_1(@NotNull String str) {
Intrinsics.checkNotNullParameter(str, "str");
return str.length();
}
这里通过Kotlin生成了
- 入参的
@NotNull
注解。 checkNotNullParameter
这个函数,检查参数是否为空,为空则抛出NPE异常。
这里会不会有什么问题?
会有问题
- 这个注解
@NotNull
是编译期注解,这个信息能被记录到class文件,但是不会在运行时被虚拟机提取,因此做运行时检查。 - 如果入参str真的是空指针null怎么办呢?Kotlin编译器可以保证源码和编译期间入参不是空的,但是没办法保证运行时这里传递进来的不是空的,因为有可能入参来自Java方法的调用,或者来自服务端数据的解析到的空指针变量。如果是空的,那么这里会抛异常,导致程序结束。
可空参数调用
// 可空参数调用
fun test_2(str: String?) = str?.length
@Nullable
public static final Integer test_2(@Nullable String str) {
return str != null ? str.length() : null;
}
这里通过Kotlin生成了
- 入参的
@Nullable
注解。 - 返回值的
@Nullable
注解。
整个函数没什么问题,我们了解一下可空变量调用?.
实际上在字节码里面还是生成了类似的if else
语句来判断空指针变量即可。
可空参数断言
// 可空参数断言
fun test_3(str: String?) = str!!.length
public static final int test_3(@Nullable String str) {
Intrinsics.checkNotNull(str);
return str.length();
}
这里通过Kotlin生成了(注解前文提过,不再赘述)
checkNotNull
这个函数。
这里我们要记住,Kotlin中的断言把可空变量直接变成了非空变量,并且底层是做空检查,有极大可能抛空指针异常。
可空参数强转非空参数
// 可空参数强转非空参数
fun test_4(str: Any?) { str as String }
public static final void test_4(@Nullable Object str) {
if (str == null) {
throw new NullPointerException("null cannot be cast to non-null type kotlin.String");
} else {
String var10000 = (String)str;
}
}
可空转非空时,Kotlin会给你做一个空指针类型的转换,如果是空的,先抛空指针异常,然后再做强转。
非空参数强转非空参数
// 非空参数强转非空参数
fun test_5(str: Any?) { str as? String }
public static final void test_5(@Nullable Object str) {
Object var10000 = str;
if (!(str instanceof String)) {
var10000 = null;
}
String var1 = (String)var10000;
}
这里有点意思,其实不会报错的,如果类型不匹配,那么直接变成空指针变量,然后再赋值给目标类型,而不是抛出类型转换失败的情况。
总结
和本小节结论一致,JVM平台上的Kotlin并不存在一个实际的非空类型,他的非空是自动生成了代码来做保护的。
Kotlin空安全潜藏问题
上述我们看到Kotlin非空类型是自动生成了检查代码来保护的。并且也提到了可能引发的问题,本节列出我们实际可能会遇到的问题。
所有问题的本质原因是:
- 变量为非空类型,但是实际传递过去的是空指针类型变量。那么会抛出空指针异常。
出现这种情况的场景有:
- 调用Java方法,返回空指针,然后用非空变量接收了。
- kotlin函数的参数是非空,但是传递进去的是空指针。
- 从服务端解析到的数据解析结果是空指针,但是用非空变量来接收。
空安全编程实战建议
我们要有一个原则,就是一定要谨慎地使用Kotlin给我们提供的非空变量类型,少有不慎将导致程序崩溃。
我们一般在自己写的代码的地方使用非空类型变量不会出问题,一般出问题都是在和其他API交互,或者调用别人写的代码的时候出的问题。我们分两个角度来阐述:
Kotlin编写API
编写API的时候我们站在编写函数的角度考虑:
fun test(input: String?): String {
return ""
}
你自己写的东西是确定的,但是调用方给你的东西是不确定的。并且调用方又希望你返回的东西是确定的。
- 提供的API的参数,要有更高的宽容度,这里你如果规定一个非空参数,但是客户在使用这个函数的时候,万一传递了一个他们自己都不知道的null进来,那么就会发生崩溃,而崩溃还是发生在你这个函数里面的,因为Kotlin会给这个参数做检查,自动生成检查的字节码。
- 返回的参数要有更高的精确度,可以的话,不要让客户以为你会返回一个可以是null的变量,这样会让客户方更好地使用你的函数。当然,返回非空变量的时候你自己要检查,这个一定不能是一个null,否则你把null返回去的时候,函数内部又会出错了。
总结一下:
- 函数参数是可空变量。
- 函数返回值是非空变量。
Kotlin使用API
在Kotlin使用API的时候,你自己的代码是确定的,你调用的函数就不一定是确定的。
因此,你不能用非空变量去接收变量。
例如:
// JavaTest.testA()返回的是null
val javaTestReceive: String = JavaTest.testA()
println(javaTestReceive)
这里在第二行会报空指针的错误,因为这里Kotlin生成的字节码是这样的:
String var10 = JavaTest.testA();
Intrinsics.checkNotNullExpressionValue(var10, "JavaTest.testA()");
String javaTestReceive = var10;
即当我们使用不确定的API的时候,最好用可空变量去接收。
会在以下的情况发生:
- 调用Java API。
- 解析服务端返回的数据,数据类型要是可空。
总结反思kotlin空安全设计
这是非常主观的一个想法。
空安全的设计是好的,但是没有理解到空安全的缺陷的时候,空安全是不安全的,甚至可能会给你带来额外的你不知道的危险。
Kotlin的空安全完全没有脱离Java的类型系统,并没有真正发明一个“非空类型”,所有的非空类型,都是Kotlin智能地给你生成代码做检查。
我个人理解,空指针这个东西是一定无法避免的,因为所有的语言最终运行在操作系统上,操作系统是C语言写的,C语言就有空指针。那底层是一定有空指针的,只是上层能够通过某种方式隐藏了空指针,或者避免了空指针异常带来的危害而已。