Java 和 Kotlin 中的可空性(Nullability)
可空性(Nullability) 是指一个变量能否为 null
值的能力. 当变量值为 null
, 使用这个变量指向的对象将会导致 NullPointerException
异常. 有很多种方法来编写代码, 来尽量减少发生指针异常的可能.
这篇向导会介绍在 Java 和在 Kotlin 中, 处理可为 null 值的变量的方案之间的区别. 这可以帮助你从 Java 迁移到 Kotlin, 并按照纯正的 Kotlin 风格来编写你的代码.
这篇向导的第一部分介绍最重要的差别 – Kotlin 对可为 null 类型的支持, 以及 Kotlin 如何处理 来自 Java 代码的类型. 第二部分, 从 检查函数调用的结果 开始, 通过几种具体的情况, 解释二者的差异.
对可为 null 类型的支持
Kotlin 和 Java 的类型系统最重要的区别是, Kotlin 明确支持 可为 null 的类型. 通过这种方式, 指定哪个变量可能包含 null
值. 如果一个变量可以为 null
, 那么对这个变量调用方法是不安全的, 因为可能导致 NullPointerException
异常. Kotlin 在编译期禁止这样的调用, 因此防止很多潜在的异常. 在运行期, 可为 null 类型的对象与不可为 null 类型的对象的处理方式是一样的: 可为 null 的类型 并不是不可为 null 类型的一个包装. 所有的检查都在编译期进行. 这就意味着, 在 Kotlin 中使用可为 null 类型, 几乎不存在运行期负担.
在 Java 中, 如果你不编写 null 检查代码, 那么方法可能会抛出 NullPointerException
异常:
这个调用会产生下面的输出:
在 Kotlin 中, 所有的通常类型默认都是不可为 null 的, 除非你将它们明确的标记为可为 null. 如果你不期望 a
可以为 null
, 可以将 stringLength()
函数声明如下:
参数 a
类型为 String
类型, 在 Kotlin 中代表它永远包含一个 String
实例, 而且不能为 null
. Kotlin 中可为 null 的类型使用问号 ?
来标记, 例如, String?
. 如果 a
是 String
类型, 那么运行期出现 NullPointerException
是不可能的, 因为编译器会强制要求所有传递给 stringLength()
的参数不能为 null
.
试图向 stringLength(a: String)
函数传递 null
值参数, 会导致编译期错误, "Null can not be a value of a non-null type String":
如果你想要向这个函数传递任意值的参数, 包括 null
值, 请在参数类型之后添加一个问号 String?
, 并在函数体内部进行检查, 以确保参数值不是 null
:
在检查通过之后, 在编译器执行检查的范围内, 编译器会将这个变量当作是不可为 null 的类型 String
.
你不进行这样的检查, 代码会编译失败, 错误信息如下: "Only safe (?.) or non-nullable asserted (!!.) calls are allowed on a nullable receiver of type String?".
这段代码还可以写得更简短一些 – 使用 安全调用操作符 ?. (If-not-null 的简写表达), 可以将 null 检查和方法调用结合为一个操作符:
平台类型(Platform types)
在 Java 中, 你可以使用注解来表示一个变量是否可以为 null
. 这些注解不是标准库的一部分, 但你可以分别添加这些注解. 例如, 你可以使用 JetBrains 注解 @Nullable
和 @NotNull
(来自 org.jetbrains.annotations
包), 或 Eclipse 的注解(org.eclipse.jdt.annotation
包). 当你 从 Kotlin 代码调用 Java 代码 时, Kotlin 能够识别这些注解, 并根据注解来处理这些类型.
如果你的 Java 代码没有这样的注解, Kotlin 会将 Java 类型当作 平台类型(Platform types). 但由于 Kotlin 没有这些类型的可空性信息, 它的编译器会允许对这些类型进行操作. 需要由你来决定是否执行 null 检查, 因为:
和 Java 中一样, 如果你试图在
null
值上执行操作, 那么会发生NullPointerException
异常.编译器不会对多余的 null 检查进行高亮度显示, 如果你对一个不可为 null 类型的值,执行 null 值安全的操作, 通常会高亮度显示.
对确定不为 null (definitely non-nullable) 类型的支持
在 Kotlin 中, 如果要覆盖一个包含 @NotNull
参数的 Java 方法, 你需要 Kotlin 的确定不为 null (definitely non-nullable) 类型.
例如, 对于 Java 中的这个 load()
方法:
要在 Kotlin 中成功的覆盖 load()
方法, 你需要将 T1
声明为确定不为 null (T1 & Any
):
关于泛型中的确定不为 null 类型, 详情请参见 确定不为 null 类型.
检查函数调用的结果
需要进行 null
值检查的一种常见的情况是, 通过函数调用得到结果.
在下面的示例中, 有 2 个 类, Order
和 Customer
. Order
包含一个指向 Customer
实例的引用. findOrder()
函数返回 Order
类的一个实例, 如果它无法找到订单, 则返回 null
值. 目标要是处理得到的订单的客户实例.
下面是 Java 中的类:
在 Java 中, 调用函数, 并对结果进行 if-not-null 检查, 然后取得需要的属性值:
如果将上面的 Java 代码直接转换为 Kotlin 代码, 结果是:
这里可以使用 安全调用操作符 ?.
(If-not-null 的简写表达), 结合标准库中的任何 作用域函数. 通常可以使用 let
函数:
下面是更加简短的版本:
使用默认值代替 null
null
值检查通常用于对值为 null
的情况 设置默认值.
带有 null 检查的 Java 代码:
要在 Kotlin 中表达同样的功能, 请使用 Elvis 操作符 (If-not-null-else 的简写表达):
返回一个值或返回 null 的函数
在 Java 中, 操作列表元素时你需要很小心. 你必须检查某个下标位置对应的元素是否存在, 然后才能使用这个元素:
Kotlin 标准库通常会提供一些函数, 其名称表示它们可能会返回 null
值. 在集合 API 中, 这样的函数尤其普遍:
聚合(Aggregate)操作
你需要得到最大元素, 或者如果不存在元素则得到 null
值, 在 Java 中你可以使用 Stream API:
在 Kotlin 中, 可以使用 聚合操作:
更多详情请参见 Java 和 Kotlin 中的集合.
安全的类型转换
如果你需要安全的转换一个类型, 在 Java 中你会使用 instanceof
操作符, 然后检查它是否成功:
在 Kotlin 中为了避免异常, 可以使用 安全的转换操作符 as?
, 它会在转换失败时返回 null
:
下一步做什么?
阅读其他的 Kotlin 惯用法.
学习如何使用 Java-to-Kotlin (J2K) 转换器, 将既有的 Java 代码转换为 Kotlin.
阅读其他的迁移向导:
如果你有喜欢的惯用法, 欢迎你发送一个 pull request, 分享给我们!