本文大纲:

  • kotlin空安全字节码原理
  • kotlin空安全潜藏问题
  • 空安全编程实战建议
  • 总结反思kotlin空安全设计

空安全 - Kotlin语言中文网中介绍了什么是Kotlin空安全,和Kotlin中可能引起NPE的场景。我们本文结合字节码来分析空安全实现原理、潜藏问题、实战建议和空安全总结。

Kotlin空安全字节码原理

参考:Kotlin刨根问底(一):你真的了解Kotlin中的空安全吗? - 掘金 (juejin.cn)

结论先行:

Java的类型本身是可以指向空的,Kotlin多了一个非空类型,是由于Kotlin在编译期生成了额外的注解代码和检查代码,才得以支持了Kotlin非空类型。

代码分析:

用以下5个函数来分析:

  1. // 非空参数调用
  2. fun test_1(str: String) = str.length
  3. // 可空参数调用
  4. fun test_2(str: String?) = str?.length
  5. // 可空参数断言
  6. fun test_3(str: String?) = str!!.length
  7. // 可空参数强转非空参数
  8. fun test_4(str: Any?) { str as String }
  9. // 非空参数强转非空参数
  10. fun test_5(str: Any?) { str as? String }

接着我们逐个分析,每个Kotlin函数都附上对应反编译的Java方法。

非空参数调用

  1. // 非空参数调用
  2. fun test_1(str: String) = str.length
  3. public static final int test_1(@NotNull String str) {
  4. Intrinsics.checkNotNullParameter(str, "str");
  5. return str.length();
  6. }

这里通过Kotlin生成了

  1. 入参的@NotNull注解。
  2. checkNotNullParameter这个函数,检查参数是否为空,为空则抛出NPE异常。

这里会不会有什么问题?
会有问题

  1. 这个注解@NotNull是编译期注解,这个信息能被记录到class文件,但是不会在运行时被虚拟机提取,因此做运行时检查。
  2. 如果入参str真的是空指针null怎么办呢?Kotlin编译器可以保证源码和编译期间入参不是空的,但是没办法保证运行时这里传递进来的不是空的,因为有可能入参来自Java方法的调用,或者来自服务端数据的解析到的空指针变量。如果是空的,那么这里会抛异常,导致程序结束。

可空参数调用

  1. // 可空参数调用
  2. fun test_2(str: String?) = str?.length
  3. @Nullable
  4. public static final Integer test_2(@Nullable String str) {
  5. return str != null ? str.length() : null;
  6. }

这里通过Kotlin生成了

  1. 入参的@Nullable注解。
  2. 返回值的@Nullable注解。

整个函数没什么问题,我们了解一下可空变量调用?.实际上在字节码里面还是生成了类似的if else语句来判断空指针变量即可。

可空参数断言

  1. // 可空参数断言
  2. fun test_3(str: String?) = str!!.length
  3. public static final int test_3(@Nullable String str) {
  4. Intrinsics.checkNotNull(str);
  5. return str.length();
  6. }

这里通过Kotlin生成了(注解前文提过,不再赘述)

  1. checkNotNull这个函数。

这里我们要记住,Kotlin中的断言把可空变量直接变成了非空变量,并且底层是做空检查,有极大可能抛空指针异常。

可空参数强转非空参数

  1. // 可空参数强转非空参数
  2. fun test_4(str: Any?) { str as String }
  3. public static final void test_4(@Nullable Object str) {
  4. if (str == null) {
  5. throw new NullPointerException("null cannot be cast to non-null type kotlin.String");
  6. } else {
  7. String var10000 = (String)str;
  8. }
  9. }

可空转非空时,Kotlin会给你做一个空指针类型的转换,如果是空的,先抛空指针异常,然后再做强转。

非空参数强转非空参数

  1. // 非空参数强转非空参数
  2. fun test_5(str: Any?) { str as? String }
  3. public static final void test_5(@Nullable Object str) {
  4. Object var10000 = str;
  5. if (!(str instanceof String)) {
  6. var10000 = null;
  7. }
  8. String var1 = (String)var10000;
  9. }

这里有点意思,其实不会报错的,如果类型不匹配,那么直接变成空指针变量,然后再赋值给目标类型,而不是抛出类型转换失败的情况。

总结

和本小节结论一致,JVM平台上的Kotlin并不存在一个实际的非空类型,他的非空是自动生成了代码来做保护的。

Kotlin空安全潜藏问题

上述我们看到Kotlin非空类型是自动生成了检查代码来保护的。并且也提到了可能引发的问题,本节列出我们实际可能会遇到的问题。

所有问题的本质原因是:

  • 变量为非空类型,但是实际传递过去的是空指针类型变量。那么会抛出空指针异常。

出现这种情况的场景有:

  1. 调用Java方法,返回空指针,然后用非空变量接收了。
  2. kotlin函数的参数是非空,但是传递进去的是空指针。
  3. 从服务端解析到的数据解析结果是空指针,但是用非空变量来接收。

空安全编程实战建议

我们要有一个原则,就是一定要谨慎地使用Kotlin给我们提供的非空变量类型,少有不慎将导致程序崩溃。

我们一般在自己写的代码的地方使用非空类型变量不会出问题,一般出问题都是在和其他API交互,或者调用别人写的代码的时候出的问题。我们分两个角度来阐述:

Kotlin编写API

编写API的时候我们站在编写函数的角度考虑:

  1. fun test(input: String?): String {
  2. return ""
  3. }

你自己写的东西是确定的,但是调用方给你的东西是不确定的。并且调用方又希望你返回的东西是确定的。

  1. 提供的API的参数,要有更高的宽容度,这里你如果规定一个非空参数,但是客户在使用这个函数的时候,万一传递了一个他们自己都不知道的null进来,那么就会发生崩溃,而崩溃还是发生在你这个函数里面的,因为Kotlin会给这个参数做检查,自动生成检查的字节码。
  2. 返回的参数要有更高的精确度,可以的话,不要让客户以为你会返回一个可以是null的变量,这样会让客户方更好地使用你的函数。当然,返回非空变量的时候你自己要检查,这个一定不能是一个null,否则你把null返回去的时候,函数内部又会出错了。

总结一下:

  1. 函数参数是可空变量。
  2. 函数返回值是非空变量。

Kotlin使用API

在Kotlin使用API的时候,你自己的代码是确定的,你调用的函数就不一定是确定的。
因此,你不能用非空变量去接收变量。
例如:

  1. // JavaTest.testA()返回的是null
  2. val javaTestReceive: String = JavaTest.testA()
  3. println(javaTestReceive)

这里在第二行会报空指针的错误,因为这里Kotlin生成的字节码是这样的:

  1. String var10 = JavaTest.testA();
  2. Intrinsics.checkNotNullExpressionValue(var10, "JavaTest.testA()");
  3. String javaTestReceive = var10;

即当我们使用不确定的API的时候,最好用可空变量去接收。
会在以下的情况发生:

  1. 调用Java API。
  2. 解析服务端返回的数据,数据类型要是可空。

总结反思kotlin空安全设计

这是非常主观的一个想法。

空安全的设计是好的,但是没有理解到空安全的缺陷的时候,空安全是不安全的,甚至可能会给你带来额外的你不知道的危险。

Kotlin的空安全完全没有脱离Java的类型系统,并没有真正发明一个“非空类型”,所有的非空类型,都是Kotlin智能地给你生成代码做检查。

我个人理解,空指针这个东西是一定无法避免的,因为所有的语言最终运行在操作系统上,操作系统是C语言写的,C语言就有空指针。那底层是一定有空指针的,只是上层能够通过某种方式隐藏了空指针,或者避免了空指针异常带来的危害而已。