在 Java 中尝尝会遇到 NullPointerException,下面简称 NPE 。如果要规避这个问题,就需要不断的添加非空判断。在kotlin中,提供了几个操作符来规避这个问题。
非空类型
在 kotlin 中直接使用 `var` 和 `val` 声明的变量和常量都是非空类型。如果没有给定初始化值,在编辑时就会报错。如图:
只要赋值为 null
,就会有问题,但是如果给其赋值一个空的字符串 ""
,则不会有问题。
可空类型
在 kotlin 中提供了一个可空操作符 `?` ,在变量类型后面添加一个 `?` 的操作符,就会将当前的变量变成可空类型。
var nullableString: String? = null
可空类型可以赋值为 null
,但是如果访问它的属性或者方法会报 NPE。
安全调用操作符
当一个变量为可空类型时,并且这个变量为空时,访问它的属性或方法时,就会抛出 NPE 。为了避免这个问题,kotlin 提供了 ?.
操作符。当一个变量为赋值为空,并且使用 ?.
修饰,这时我们再去访问它的方法或者属性,这时就不会触发 NPE 。
var nullableString: String? = null
fun main() {
println(nullableString?.length)
}
上面的代码并不会触发空指针异常,而是会输出 null
。
当我们使用kotlin提供的 `Show Kotlin Bytecode` 的工具来看 `?.` 的实现方式就会发现,它实际上是先对 `nullableString` 的副本进行判空,如果不为空才会去访问 `nullableString` 的 `length` 属性。
如下的代码显示,kotlin 代码其实也是Java代码,它的文件名就是Java的类名,其中的变量,会自动添加 `set/get` 方法。并且会在其变量和 `set/get` 方法上加上`org.jetbrains.annotations.Nullable` 注解。
public final class NonNullSampleKt {
@Nullable
private static String nullableString;
@Nullable
public static final String getNullableString() {
return nullableString;
}
public static final void setNullableString(@Nullable String var0) {
nullableString = var0;
}
public static final void main() {
// 声明一个新的局部变量
String var10000 = nullableString;
// 就其进行判空操作,如果不为空就去获取 length() 的值
Integer var0 = var10000 != null ? var10000.length() : null;
boolean var1 = false;
System.out.println(var0);
}
// $FF: synthetic method
public static void main(String[] var0) {
main();
}
}
安全调用操作符除了上面的作用之外,它还可以**跟 内联函数`let` 一起使用**。
一般情况下,我们直接使用 `let` 函数,如果要在其作用域中访问其内容时容易出现下面的问题:
如果我在调用 `let` 函数时加上 `?` 操作符,就表示在 `string` 不为空的条件下才会执行 `let` 函数体内的函数体。
val string: String? = "Hello World"
string?.let {
print(it[0])
}
这样就可以很愉快的使用 let
函数了。
另外 ?.
操作是可以链式调用的。例如,如果一个员工 Bob 可能会(或者不会)分配给一个部门, 并且可能有另外一个员工是该部门的负责人,那么获取 Bob 所在部门负责人(如果有的话)的名字,那么我们可以这样写:
bob?.department?.head?.name
如果上述的一个环节为空,这个链式调用就会返回 `null`。
非空断言运算符
非空断言运算符 !!
,会将任何值转换为非空类型,如果该值为空,就会抛出 NPE。下面的代码就会抛出 NPE。
var nullableString: String? = null
fun main() {
// nullableString 可空类型,如果直接调用就会产生编译错误
// println(nullableString.length)
println(nullableString!!.length)
}
上面的代码中,`nullableString` 为空,如果直接访问 `length` 在编译期就会差生错误提示:`Only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type String`。根据提示,我们可以看出当前的 `nullableString` 这个可空类型,必须通过 `!!` 或者 `?.` 操作符才可以调用其属性或者方法。
当我们使用了 `!!` 修饰了 `nullableString` 后,我们可以正常调用 `length` 属性,但是 `!!` 运算符并不是安全的,你需要在使用时进行非空判断。
同样的我们通过 kotlin 提供的 `Show Kotlin Bytecode` 的工具来看一下,其它反编译后的Java代码,如下:
public final class NonNullSampleKt {
@Nullable
private static String nullableString;
@Nullable
public static final String getNullableString() {
return nullableString;
}
public static final void setNullableString(@Nullable String var0) {
nullableString = var0;
}
public static final void main() {
String var10000 = nullableString;
if (var10000 == null) {
// 这里会抛出一个 NPE 。
Intrinsics.throwNpe();
}
int var0 = var10000.length();
boolean var1 = false;
System.out.println(var0);
}
// $FF: synthetic method
public static void main(String[] var0) {
main();
}
}
通过上面代码我们可以看到,`?.` 和 `!!` 的实现方式很接近。 跟 `?.` 不同的是,`!!` 会先判断 `nullableString` 是否为空,如果为空会直接抛出 NPE,如果不为空才会走接下来的操作。`!!` 运算符适合那些非常喜欢对变量进行非空操作的人。
三目运算符
在 kotlin 中,三目运算符有两种形式,一种是由 if ... else
演变而来;另一种则是 使用 ?:
操作符来实现。
val l = if (nullableString != null) nullableString?.length else -1
println(l)
上面的形式就是由 `if ... else` 演变而来,只是省略了其中的`{}` ,判断条件和 `else` 后面只能跟一行代码,逻辑也是一目了然。
第二种方式的三目运算主要作用的在对象的引用上,并不能根据某个对象的值来做下一步的判断,只能判断某个对象的引用是否空。
`?:` 又叫做 Elvis 操作符,它的含义是:如果 `?:` 左侧的表达式非空,就返回左侧表达式;否则就返回右侧表达式。**注意,当且仅当左侧为空时,才会对右侧表达式求值**。
val len = nullableString?.length ?: -1
println(len)
上面的代码中如果 `nullableString` 为空则 `nullableString?.length` 会返回空,这时就会返回 `?:` 右侧的表达式,所以最后 `len` 的值为 -1。如果 `nullableString` 不为空,则会直接返回 `nullableString?.length` 的值。
`?:` 运算符有个特点,就是:**当且仅当左侧为空时,才会对右侧表达式求值**。看下面的代码
var a: Int? = null
val b = a == 1 ?: 0
println(b)
上面的代码中个,虽然变量 `a` 是空的,但是 `a==1` 这个操作并不会返回空,它会先判断 `==` 两端的值是否都为空,然后在去判断它们的值是否相等,不管最后的结果怎样,它都会返回一个 `Boolean` 类型的值,并不会返回空。所以 `a==1` 这个操作始终都是非空的,所以 `b` 的值一直会与 `a==1` 相同。同样的 `===` 运算符 也会导致同样的情况。
安全的类型转换
在 kotlin 中使用 `as` 关键字进行类型转换,它的实现方式就是 Java 中的强制类型转换,如下代码
var a : Any? =1
val b = a as String
println(b)
上面代码中,变量 a
的数据类型无法确定,我们声明为 Any
类型(类似 Java 中的 Object 类型)。使用 as
将它转换为 String
类型 , as
的实现方式就是强制类型转换,但是我们通过变量 a
的赋值可以看到,它是 Int
类型,如果强制转换为 String
类型一定会抛出 ClassCastException
。当然事实也是如此。
我们通过 `Show Kotlin Bytecode` 工具查看一下它的Java实现。
Object a = 1;
String b = (String)a;
boolean var2 = false;
System.out.println(b);
很明显我们可以看出,将 `int` 类型强制转换为 `String` 类型,一定会抛出 `ClassCastException` 。
为了避免上面的问题, `kotlin` 提供了 `as?` 操作符,使用它就类似Java在进行类型转换之前会进行 `instanceof` 判断,如果转换的不是目标类型,就会返回空。
var a: Any? = 1
val b = a as? String
println(b)
上面的代码就会输出 null
,而不会抛出 ClassCastException
异常。我们再通过 Show Kotlin Bytecode
工具查看一下它的Java实现:
Object a = 1;
Integer var10000 = a;
if (!(a instanceof String)) {
var10000 = null;
}
String b = (String)var10000;
boolean var2 = false;
System.out.println(b);
很明显可以看到,在Java实现中,先新建一个副本来存储 a
的值,然后通过 instanceof
判断副本的类型是为目标类型,如果不是则赋值为 null
,后期转换时就用 null
进行转换。这样就保证了其不会抛出 ClassCastException
异常。
总结
kotlin 为了解决空安全的问题,提供了 安全调用符`?.` ,只有对象不为空时才会执行后面的操作,这样就简化了非空判断的操作,安全调用操作符`?.` 是可以在链式调用中非常有用。同样的在强制类型转换时使用 `as?` 操作符,也会避免掉类型不符的问题。
特别需要注意的是 `!!` 操作符并不是安全的,使用时一定要做好判空操作。在使用 Elvis 操作符时一定要注意左侧表达式,有些情况下它总是非空的。