可空类型是Kotlin类型系统的一个特性,主要是为了解决Java中的令人头疼的 NullPointerException 问题。我们知道,在Java中如果一个变量可以是null,来那么使用它调用一个方法就是不安全的,因为它会导致:NullPointerException 。
Kotlin把可空性(nullability)作为类型系统的一部分,Kotlin编译器可以直接在编译过程中发现许多可能的错误,并减少在运行时抛出异常的可能性。Kotlin的类型系统和Java相比,首要的区别就是Kotlin对可空类型的显式支持。在本节中,我们将讨论Kotlin中的可空类型。
4.5.1 null 是什么
对于Java程序员来说,null是令人头痛的东西。我们时常会受到空指针异常(NPE)的骚扰。就连Java的发明者都承认这是他的一项巨大失误。Java为什么要保留null呢?null出现有一段时间了,并且我认为Java发明者知道null与它解决的问题相比带来了更多的麻烦,但是null仍然陪伴着Java。
我们通常把null理解为编程语言中定义特殊的0, 把我们初始化的指针指向它,以防止“野指针”的恶果。在Java中,null是任何引用类型的默认值,不严格的说是所有Object类型的默认值。
这里的null既不是对象也不是一种类型,它仅是一种特殊的值,我们可以将其赋予任何引用类型,也可以将null转化成任何类型。在编译和运行时期,将null强制转换成任何引用类型都是可行的,在运行时期都不会抛出空指针异常。注意,这里指的是任何Java的引用类型。在遇到基本类型int long float double short byte 等的时候,情况就不一样了。而且还是个坑。编译器不会报错,但是运行时会抛NPE。空指针异常。这是Java中的自动拆箱导致的。代码示例:
Integer nullInt = null; // this is ok
int anotherInt = nullInt; // 编译器允许这么赋值, 但是在运行时抛 NullPointerException
所以,我们写Java代码的时候,要时刻注意这一点:Integer的默认值是null而不是0。当把null值传递给一个int型变量的时候,Java的自动装箱将会返回空指针异常。
4.5.2 Kotlin 中的 null
在Kotlin中,针对Java中的null的杂乱局面,进行了整顿,作了清晰的界定,并在编译器级别强制规范了可空null变量类型的使用。
我们来看一下Kotlin中关于null的一些有趣的运算。
null跟null是相等的:
>>> null==null
true
>>> null!=null
false
null这个值比较特殊,null 不是Any类型
>>> null is Any
false
但是,null是Any?类型:
>>> null is Any?
true
我们来看看null对应的类型到底是什么:
>>> var a=null
>>> a
null
>>> a=1
error: the integer literal does not conform to the expected type Nothing?
a=1
^
从报错信息我们可以看出,null的类型是Nothing?。关于Nothing?我们将会在下一小节中介绍。
我们可以对null进行加法运算:
>>> "1"+null
1null
>>> null+20
null20
对应的重载运算符的函数定义在kotlin/Library.kt里面:
package kotlin
import kotlin.internal.PureReifiable
/**
* Returns a string representation of the object. Can be called with a null receiver, in which case
* it returns the string "null".
*/
public fun Any?.toString(): String
/**
* Concatenates this string with the string representation of the given [other] object. If either the receiver
* or the [other] object are null, they are represented as the string "null".
*/
public operator fun String?.plus(other: Any?): String
...
但是,反过来就不行了:
>>> 1+null
error: none of the following functions can be called with the arguments supplied:
public final operator fun plus(other: Byte): Int defined in kotlin.Int
public final operator fun plus(other: Double): Double defined in kotlin.Int
public final operator fun plus(other: Float): Float defined in kotlin.Int
public final operator fun plus(other: Int): Int defined in kotlin.Int
public final operator fun plus(other: Long): Long defined in kotlin.Int
public final operator fun plus(other: Short): Int defined in kotlin.Int
1+null
^
这是因为Int没有重载传入null参数的plus()函数。
4.5.3 可空类型 String? 与安全调用 ?.
我们来看一个例子。下面是计算字符串长度的简单Java方法:
public static int getLength1(String str) {
return str.length();
}
我们已经习惯了在这样的Java代码中,加上这样的空判断处理:
public static int getLength2(String str) throws Exception {
if (null == str) {
throw new Exception("str is null");
}
return str.length();
}
而在Kotlin中,当我们同样写一个可能为null参数的函数时:
fun getLength1(str: String): Int {
return str.length
}
当我们传入一个null参数时:
@Test fun testGetLength1() {
val StringUtilKt = StringUtilKt()
StringUtilKt.getLength1(null)
}
编译器就直接编译失败:
e: /Users/jack/easykotlin/chapter4_type_system/src/test/kotlin/com/easy/kotlin/StringUtilKtTest.kt: (15, 33): Null can not be a value of a non-null type String
:compileTestKotlin FAILED
FAILURE: Build failed with an exception.
* What went wrong:
Execution failed for task ':compileTestKotlin'.
> Compilation error. See log for more details
如果我们使用IDEA,会在编码时就直接提示错误了:
这样通过编译时强制排除空指针的错误,大大减少了出现NPE的可能。另外,如果我们确实需要传入一个可空的参数,我们可以使用可空类型String?来声明一个可以指向空指针的变量。可空类型可以用来标记任何一个变量,来表明这个变量是可空的(Nullable)。例如:Char?, Int?, MineType?(自定义的类型)等等。
我们用示例代码来更加简洁的说明:
>>> var x:String="x"
>>> x=null
error: null can not be a value of a non-null type String
x=null
^
>>> var y:String?="y"
>>> y=null
>>> y
null
我们可以看出:普通String类型,是不允许指向null的;而可空String?类可以指向null。
下面我们来尝试使用一个可空变量来调用函数:
>>> fun getLength2(str: String?): Int? = str.length
error: only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type String?
fun getLength2(str: String?): Int? = str.length
编译器直接报错,告诉我们,变量str: String?是可空的类型,调用只能通过安全调用?. 或者 非空断言调用!!. 。另外,如果不需要捕获异常来处理,我们可以使用Kotlin里面的安全调用符?. 。
代码示例:
fun getLength2(str: String?): Int? {
return str?.length
}
测试代码:
@Test fun testGetLength2() {
val StringUtilKt = StringUtilKt()
println(StringUtilKt.getLength2(null)) //null
Assert.assertTrue(3 == StringUtilKt.getLength2("abc"))
}
我们可以看出,当我们使用安全调用?. , 代码安静的执行输出了null。如果,我们确实想写一个出现空指针异常的代码,那就使用可能出现空指针的断言调用符!!. 。
代码示例:
fun getLength3(str: String?): Int? {
return str!!.length
}
测试代码:
@Test fun testGetLength3() {
val StringUtilKt = StringUtilKt()
println(StringUtilKt.getLength3(null))
Assert.assertTrue(3 == StringUtilKt.getLength3("abc"))
}
上面的代码就跟Java里面差不多了,运行会直接抛出空指针异常:
kotlin.KotlinNullPointerException
at com.easy.kotlin.StringUtilKt.getLength3(StringUtilKt.kt:16)
at com.easy.kotlin.StringUtilKtTest.testGetLength3(StringUtilKtTest.kt:28)
这里的KotlinNullPointerException 是KotlinNullPointerException.java代码,继承了Java中的java.lang.NullPointerException, 它的源代码如下:
package kotlin;
public class KotlinNullPointerException extends NullPointerException {
public KotlinNullPointerException() {
}
public KotlinNullPointerException(String message) {
super(message);
}
}
另外,如果异常需要捕获到进行特殊处理的场景,在Kotlin中仍然使用 try ... catch 捕获并处理异常。
4.5.4 可空性的实现原理
我们来看一段Kotlin的可空类型的示例代码如下:
fun testNullable1(x: String, y: String?): Int {
return x.length
}
fun testNullable2(x: String, y: String?): Int? {
return y?.length
}
fun testNullable3(x: String, y: String?): Int? {
return y!!.length
}
我们来使用IDEA的Kotlin插件来看下可空类型的安全调用的等价Java代码。
打开IDEA的 Tools > Kotlin > Show Kotlin Bytecode
然后,点击Decompile , 我们可以得到反编译的Java代码
public final class NullableTypesKt {
public static final int testNullable1(@NotNull String x, @Nullable String y) {
Intrinsics.checkParameterIsNotNull(x, "x");
return x.length();
}
@Nullable
public static final Integer testNullable2(@NotNull String x, @Nullable String y) {
Intrinsics.checkParameterIsNotNull(x, "x");
return y != null?Integer.valueOf(y.length()):null;
}
@Nullable
public static final Integer testNullable3(@NotNull String x, @Nullable String y) {
Intrinsics.checkParameterIsNotNull(x, "x");
if(y == null) {
Intrinsics.throwNpe();
}
return Integer.valueOf(y.length());
}
}
在不可空变量调用函数之前,都检查了是否为空, 使用的是kotlin.jvm.internal.Intrinsics这个Java类里面的checkParameterIsNotNull方法。如果是null就抛出异常:
public static void checkParameterIsNotNull(Object value, String paramName) {
if (value == null) {
throwParameterIsNullException(paramName);
}
}
同时,我们可以看出在Kotlin中函数的入参声明
fun testNullable(x: String, y: String?)
反编译成等价的Java代码是
public static final void testNullable(@NotNull String x, @Nullable String y)
我们可以看出,这里使用注解@NotNull标注不可空的变量,使用注解@Nullable标注一个变量可空。可空变量的安全调用符y?.length 等价的Java代码就是:
y != null?Integer.valueOf(y.length()):null
可空变量的断言调用y!!.length等价的Java代码是:
if(y == null) {
Intrinsics.throwNpe();
}
return Integer.valueOf(y.length());
4.5.5 可空类型层次体系
就像Any是在非空类型层次结构的根,
Any?是可空类型层次的根。
由于Any?是Any的超集,所以,Any?是Kotlin的类型层次结构的最顶端。
代码示例:
>>> 1 is Any
true
>>> 1 is Any?
true
>>> null is Any
false
>>> null is Any?
true
>>> Any() is Any?
true