在 Java 中尝尝会遇到 NullPointerException,下面简称 NPE 。如果要规避这个问题,就需要不断的添加非空判断。在kotlin中,提供了几个操作符来规避这个问题。

非空类型

  1. kotlin 中直接使用 `var` `val` 声明的变量和常量都是非空类型。如果没有给定初始化值,在编辑时就会报错。如图:

var-val-non-null.png

只要赋值为 null ,就会有问题,但是如果给其赋值一个空的字符串 "" ,则不会有问题。

可空类型

  1. kotlin 中提供了一个可空操作符 `?` ,在变量类型后面添加一个 `?` 的操作符,就会将当前的变量变成可空类型。
  1. var nullableString: String? = null

可空类型可以赋值为 null ,但是如果访问它的属性或者方法会报 NPE。

安全调用操作符

当一个变量为可空类型时,并且这个变量为空时,访问它的属性或方法时,就会抛出 NPE 。为了避免这个问题,kotlin 提供了 ?. 操作符。当一个变量为赋值为空,并且使用 ?. 修饰,这时我们再去访问它的方法或者属性,这时就不会触发 NPE 。

  1. var nullableString: String? = null
  2. fun main() {
  3. println(nullableString?.length)
  4. }

上面的代码并不会触发空指针异常,而是会输出 null

  1. 当我们使用kotlin提供的 `Show Kotlin Bytecode` 的工具来看 `?.` 的实现方式就会发现,它实际上是先对 `nullableString` 的副本进行判空,如果不为空才会去访问 `nullableString` `length` 属性。
  2. 如下的代码显示,kotlin 代码其实也是Java代码,它的文件名就是Java的类名,其中的变量,会自动添加 `set/get` 方法。并且会在其变量和 `set/get` 方法上加上`org.jetbrains.annotations.Nullable` 注解。
  1. public final class NonNullSampleKt {
  2. @Nullable
  3. private static String nullableString;
  4. @Nullable
  5. public static final String getNullableString() {
  6. return nullableString;
  7. }
  8. public static final void setNullableString(@Nullable String var0) {
  9. nullableString = var0;
  10. }
  11. public static final void main() {
  12. // 声明一个新的局部变量
  13. String var10000 = nullableString;
  14. // 就其进行判空操作,如果不为空就去获取 length() 的值
  15. Integer var0 = var10000 != null ? var10000.length() : null;
  16. boolean var1 = false;
  17. System.out.println(var0);
  18. }
  19. // $FF: synthetic method
  20. public static void main(String[] var0) {
  21. main();
  22. }
  23. }
  1. 安全调用操作符除了上面的作用之外,它还可以**跟 内联函数`let` 一起使用**。
  2. 一般情况下,我们直接使用 `let` 函数,如果要在其作用域中访问其内容时容易出现下面的问题:

let-fun.png

  1. 如果我在调用 `let` 函数时加上 `?` 操作符,就表示在 `string` 不为空的条件下才会执行 `let` 函数体内的函数体。
  1. val string: String? = "Hello World"
  2. string?.let {
  3. print(it[0])
  4. }

这样就可以很愉快的使用 let 函数了。

另外 ?. 操作是可以链式调用的。例如,如果一个员工 Bob 可能会(或者不会)分配给一个部门, 并且可能有另外一个员工是该部门的负责人,那么获取 Bob 所在部门负责人(如果有的话)的名字,那么我们可以这样写:

  1. bob?.department?.head?.name
  1. 如果上述的一个环节为空,这个链式调用就会返回 `null`

非空断言运算符

非空断言运算符 !! ,会将任何值转换为非空类型,如果该值为空,就会抛出 NPE。下面的代码就会抛出 NPE。

  1. var nullableString: String? = null
  2. fun main() {
  3. // nullableString 可空类型,如果直接调用就会产生编译错误
  4. // println(nullableString.length)
  5. println(nullableString!!.length)
  6. }
  1. 上面的代码中,`nullableString` 为空,如果直接访问 `length` 在编译期就会差生错误提示:`Only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type String`。根据提示,我们可以看出当前的 `nullableString` 这个可空类型,必须通过 `!!` 或者 `?.` 操作符才可以调用其属性或者方法。
  2. 当我们使用了 `!!` 修饰了 `nullableString` 后,我们可以正常调用 `length` 属性,但是 `!!` 运算符并不是安全的,你需要在使用时进行非空判断。
  3. 同样的我们通过 kotlin 提供的 `Show Kotlin Bytecode` 的工具来看一下,其它反编译后的Java代码,如下:
  1. public final class NonNullSampleKt {
  2. @Nullable
  3. private static String nullableString;
  4. @Nullable
  5. public static final String getNullableString() {
  6. return nullableString;
  7. }
  8. public static final void setNullableString(@Nullable String var0) {
  9. nullableString = var0;
  10. }
  11. public static final void main() {
  12. String var10000 = nullableString;
  13. if (var10000 == null) {
  14. // 这里会抛出一个 NPE 。
  15. Intrinsics.throwNpe();
  16. }
  17. int var0 = var10000.length();
  18. boolean var1 = false;
  19. System.out.println(var0);
  20. }
  21. // $FF: synthetic method
  22. public static void main(String[] var0) {
  23. main();
  24. }
  25. }
  1. 通过上面代码我们可以看到,`?.` `!!` 的实现方式很接近。 `?.` 不同的是,`!!` 会先判断 `nullableString` 是否为空,如果为空会直接抛出 NPE,如果不为空才会走接下来的操作。`!!` 运算符适合那些非常喜欢对变量进行非空操作的人。

三目运算符

在 kotlin 中,三目运算符有两种形式,一种是由 if ... else 演变而来;另一种则是 使用 ?: 操作符来实现。

  1. val l = if (nullableString != null) nullableString?.length else -1
  2. println(l)
  1. 上面的形式就是由 `if ... else` 演变而来,只是省略了其中的`{}` ,判断条件和 `else` 后面只能跟一行代码,逻辑也是一目了然。
  2. 第二种方式的三目运算主要作用的在对象的引用上,并不能根据某个对象的值来做下一步的判断,只能判断某个对象的引用是否空。
  3. `?:` 又叫做 Elvis 操作符,它的含义是:如果 `?:` 左侧的表达式非空,就返回左侧表达式;否则就返回右侧表达式。**注意,当且仅当左侧为空时,才会对右侧表达式求值**。
  1. val len = nullableString?.length ?: -1
  2. println(len)
  1. 上面的代码中如果 `nullableString` 为空则 `nullableString?.length` 会返回空,这时就会返回 `?:` 右侧的表达式,所以最后 `len` 的值为 -1。如果 `nullableString` 不为空,则会直接返回 `nullableString?.length` 的值。
  2. `?:` 运算符有个特点,就是:**当且仅当左侧为空时,才会对右侧表达式求值**。看下面的代码
  1. var a: Int? = null
  2. val b = a == 1 ?: 0
  3. println(b)
  1. 上面的代码中个,虽然变量 `a` 是空的,但是 `a==1` 这个操作并不会返回空,它会先判断 `==` 两端的值是否都为空,然后在去判断它们的值是否相等,不管最后的结果怎样,它都会返回一个 `Boolean` 类型的值,并不会返回空。所以 `a==1` 这个操作始终都是非空的,所以 `b` 的值一直会与 `a==1` 相同。同样的 `===` 运算符 也会导致同样的情况。

安全的类型转换

  1. kotlin 中使用 `as` 关键字进行类型转换,它的实现方式就是 Java 中的强制类型转换,如下代码
  1. var a : Any? =1
  2. val b = a as String
  3. println(b)

上面代码中,变量 a 的数据类型无法确定,我们声明为 Any 类型(类似 Java 中的 Object 类型)。使用 as 将它转换为 String 类型 , as 的实现方式就是强制类型转换,但是我们通过变量 a 的赋值可以看到,它是 Int 类型,如果强制转换为 String 类型一定会抛出 ClassCastException 。当然事实也是如此。

  1. 我们通过 `Show Kotlin Bytecode` 工具查看一下它的Java实现。
  1. Object a = 1;
  2. String b = (String)a;
  3. boolean var2 = false;
  4. System.out.println(b);
  1. 很明显我们可以看出,将 `int` 类型强制转换为 `String` 类型,一定会抛出 `ClassCastException`
  2. 为了避免上面的问题, `kotlin` 提供了 `as?` 操作符,使用它就类似Java在进行类型转换之前会进行 `instanceof` 判断,如果转换的不是目标类型,就会返回空。
  1. var a: Any? = 1
  2. val b = a as? String
  3. println(b)

上面的代码就会输出 null,而不会抛出 ClassCastException 异常。我们再通过 Show Kotlin Bytecode 工具查看一下它的Java实现:

  1. Object a = 1;
  2. Integer var10000 = a;
  3. if (!(a instanceof String)) {
  4. var10000 = null;
  5. }
  6. String b = (String)var10000;
  7. boolean var2 = false;
  8. System.out.println(b);

很明显可以看到,在Java实现中,先新建一个副本来存储 a 的值,然后通过 instanceof 判断副本的类型是为目标类型,如果不是则赋值为 null ,后期转换时就用 null 进行转换。这样就保证了其不会抛出 ClassCastException 异常。

总结

  1. kotlin 为了解决空安全的问题,提供了 安全调用符`?.` ,只有对象不为空时才会执行后面的操作,这样就简化了非空判断的操作,安全调用操作符`?.` 是可以在链式调用中非常有用。同样的在强制类型转换时使用 `as?` 操作符,也会避免掉类型不符的问题。
  2. 特别需要注意的是 `!!` 操作符并不是安全的,使用时一定要做好判空操作。在使用 Elvis 操作符时一定要注意左侧表达式,有些情况下它总是非空的。

空安全