Edit Page

Java 和 Kotlin 中的可空性(Nullability)

最终更新: 2024/03/21

可空性(Nullability) 是指一个变量能否为 null 值的能力. 当变量值为 null, 使用这个变量指向的对象将会导致 NullPointerException 异常. 有很多种方法来编写代码, 来尽量减少发生指针异常的可能.

这篇向导会介绍在 Java 和在 Kotlin 中, 处理可为 null 值的变量的方案之间的区别. 这可以帮助你从 Java 迁移到 Kotlin, 并按照纯正的 Kotlin 风格来编写你的代码.

这篇向导的第一部分介绍最重要的差别 – Kotlin 对可为 null 类型的支持, 以及 Kotlin 如何处理 来自 Java 代码的类型. 第二部分, 从 检查函数调用的结果 开始, 通过几种具体的情况, 解释二者的差异.

参见 Kotlin 中的 null 值安全性.

对可为 null 类型的支持

Kotlin 和 Java 的类型系统最重要的区别是, Kotlin 明确支持 可为 null 的类型. 通过这种方式, 指定哪个变量可能包含 null 值. 如果一个变量可以为 null, 那么对这个变量调用方法是不安全的, 因为可能导致 NullPointerException 异常. Kotlin 在编译期禁止这样的调用, 因此防止很多潜在的异常. 在运行期, 可为 null 类型的对象与不可为 null 类型的对象的处理方式是一样的: 可为 null 的类型 并不是不可为 null 类型的一个包装. 所有的检查都在编译期进行. 这就意味着, 在 Kotlin 中使用可为 null 类型, 几乎不存在运行期负担.

我们说 "几乎", 是因为, 尽管 确实 生成了 内在的 检查代码, 但它们在运行期的负担是非常非常小的.

在 Java 中, 如果你不编写 null 检查代码, 那么方法可能会抛出 NullPointerException 异常:

// Java
int stringLength(String a) {
    return a.length();
}

void main() {
    stringLength(null); // 这里会抛出 `NullPointerException` 异常
}

这个调用会产生下面的输出:

java.lang.NullPointerException: Cannot invoke "String.length()" because "a" is null
    at test.java.Nullability.stringLength(Nullability.java:8)
    at test.java.Nullability.main(Nullability.java:12)
    at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
    at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)

在 Kotlin 中, 所有的通常类型默认都是不可为 null 的, 除非你将它们明确的标记为可为 null. 如果你不期望 a 可以为 null, 可以将 stringLength() 函数声明如下:

// Kotlin
fun stringLength(a: String) = a.length

参数 a 类型为 String 类型, 在 Kotlin 中代表它永远包含一个 String 实例, 而且不能为 null. Kotlin 中可为 null 的类型使用问号 ? 来标记, 例如, String?. 如果 aString 类型, 那么运行期出现 NullPointerException 是不可能的, 因为编译器会强制要求所有传递给 stringLength() 的参数不能为 null.

试图向 stringLength(a: String) 函数传递 null 值参数, 会导致编译期错误, "Null can not be a value of a non-null type String":

向不可为 null 的函数传递 null 值参数时的错误

如果你想要向这个函数传递任意值的参数, 包括 null 值, 请在参数类型之后添加一个问号 String?, 并在函数体内部进行检查, 以确保参数值不是 null:

// Kotlin
fun stringLength(a: String?): Int = if (a != null) a.length else 0

在检查通过之后, 在编译器执行检查的范围内, 编译器会将这个变量当作是不可为 null 的类型 String.

你不进行这样的检查, 代码会编译失败, 错误信息如下: "Only safe (?.) or non-nullable asserted (!!.) calls are allowed on a nullable receiver of type String?".

这段代码还可以写得更简短一些 – 使用 安全调用操作符 ?. (If-not-null 的简写表达), 可以将 null 检查和方法调用结合为一个操作符:

// Kotlin
fun stringLength(a: String?): Int = a?.length ?: 0

平台类型(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 值安全的操作, 通常会高亮度显示.

更多详情请参见 从 Kotlin 调用 Java 代码时, 如何处理 null 值安全性与平台类型.

对确定不为 null (definitely non-nullable) 类型的支持

在 Kotlin 中, 如果要覆盖一个包含 @NotNull 参数的 Java 方法, 你需要 Kotlin 的确定不为 null (definitely non-nullable) 类型.

例如, 对于 Java 中的这个 load() 方法:

import org.jetbrains.annotations.*;

public interface Game<T> {
  public T save(T x) {}
  @NotNull
  public T load(@NotNull T x) {}
}

要在 Kotlin 中成功的覆盖 load() 方法, 你需要将 T1 声明为确定不为 null (T1 & Any):

interface ArcadeGame<T1> : Game<T1> {
  override fun save(x: T1): T1
  // T1 声明为确定不为 null
  override fun load(x: T1 & Any): T1 & Any
}

关于泛型中的确定不为 null 类型, 详情请参见 确定不为 null 类型.

检查函数调用的结果

需要进行 null 值检查的一种常见的情况是, 通过函数调用得到结果.

在下面的示例中, 有 2 个 类, OrderCustomer. Order 包含一个指向 Customer 实例的引用. findOrder() 函数返回 Order 类的一个实例, 如果它无法找到订单, 则返回 null 值. 目标要是处理得到的订单的客户实例.

下面是 Java 中的类:

//Java
record Order (Customer customer) {}

record Customer (String name) {}

在 Java 中, 调用函数, 并对结果进行 if-not-null 检查, 然后取得需要的属性值:

// Java
Order order = findOrder();

if (order != null) {
    processCustomer(order.getCustomer());
}

如果将上面的 Java 代码直接转换为 Kotlin 代码, 结果是:

// Kotlin
data class Order(val customer: Customer)

data class Customer(val name: String)

val order = findOrder()

// 直接转换
if (order != null){
    processCustomer(order.customer)
}

这里可以使用 安全调用操作符 ?. (If-not-null 的简写表达), 结合标准库中的任何 作用域函数. 通常可以使用 let 函数:

// Kotlin
val order = findOrder()

order?.let {
    processCustomer(it.customer)
}

下面是更加简短的版本:

// Kotlin
findOrder()?.customer?.let(::processCustomer)

使用默认值而不是 null

null 值检查通常用于对值为 null 的情况 设置默认值.

带有 null 检查的 Java 代码:

// Java
Order order = findOrder();
if (order == null) {
    order = new Order(new Customer("Antonio"))
}

要在 Kotlin 中表达同样的功能, 请使用 Elvis 操作符 (If-not-null-else 的简写表达):

// Kotlin
val order = findOrder() ?: Order(Customer("Antonio"))

返回一个值或返回 null 的函数

在 Java 中, 操作列表元素时你需要很小心. 你必须检查某个下标位置对应的元素是否存在, 然后才能使用这个元素:

// Java
var numbers = new ArrayList<Integer>();
numbers.add(1);
numbers.add(2);

System.out.println(numbers.get(0));
//numbers.get(5) // 这里会发生异常!

Kotlin 标准库通常会提供一些函数, 其名称表示它们可能会返回 null 值. 在集合 API 中, 这样的函数尤其普遍:

fun main() {
//sampleStart
    // Kotlin
    // 和 Java 中一样的代码:
    val numbers = listOf(1, 2)
    
    println(numbers[0])  // 如果集合为空, 会抛出 IndexOutOfBoundsException 异常
    //numbers.get(5)     // 这里会发生异常!

    // 更加智能的代码:
    println(numbers.firstOrNull())
    println(numbers.getOrNull(5)) // 这里会返回 null 值
//sampleEnd
}

聚合(Aggregate)操作

你需要得到最大元素, 或者如果不存在元素则得到 null 值, 在 Java 中你可以使用 Stream API:

// Java
var numbers = new ArrayList<Integer>();
var max = numbers.stream().max(Comparator.naturalOrder()).orElse(null);
System.out.println("Max: " + max);

在 Kotlin 中, 可以使用 聚合操作:

// Kotlin
val numbers = listOf<Int>()
println("Max: ${numbers.maxOrNull()}")

更多详情请参见 Java 和 Kotlin 中的集合.

安全的类型转换

如果你需要安全的转换一个类型, 在 Java 中你会使用 instanceof 操作符, 然后检查它是否成功:

// Java
int getStringLength(Object y) {
    return y instanceof String x ? x.length() : -1;
}

void main() {
    System.out.println(getStringLength(1)); // 输出结果为 `-1`
}

在 Kotlin 中为了避免异常, 可以使用 安全的转换操作符 as?, 它会在转换失败时返回 null:

// Kotlin
fun main() {
    println(getStringLength(1)) // 输出结果为 `-1`
}

fun getStringLength(y: Any): Int {
    val x: String? = y as? String // 结果为 null
    return x?.length ?: -1 // 返回 -1, 因为 `x` 为 null
}

在上面的 Java 示例中, getStringLength() 函数返回的结果是基本类型 int. 如果要让它返回 null, 你可以使用 装箱(boxed) 类型 Integer. 但是, 让这样的函数返回一个负值, 然后检查结果值, 在资源方面效率更高 – 如论如何你都需要进行检查, 但这种方式不会发生额外的装箱操作.

下一步做什么?

如果你有喜欢的惯用法, 欢迎发起一个 PR, 分享给我们!