Java 与 Kotlin 空安全对比

空安全是 Java 迁移 Kotlin 时最需要认真理解的主题。Kotlin 的目标不是让 null 消失,而是让“哪里可能为 null”成为类型契约的一部分。

Java 的问题

Java 引用类型默认都可能是 null

int length(String text) {
    return text.length();
}

调用方可以传入 null

length(null); // 运行时 NullPointerException

你可以通过注解、文档、测试和代码审查降低风险,但 Java 语言本身不会强制你处理所有空值路径。

Kotlin 的非空默认

Kotlin 普通类型默认非空:

fun length(text: String): Int {
    return text.length
}

调用方不能传入 null

// length(null) // 编译错误

如果允许为空,必须显式声明:

fun length(text: String?): Int {
    return text?.length ?: 0
}

运行时没有“可空包装”

StringString? 的差异主要存在于编译期类型系统。运行时对象本身并不会因为 String? 多一层包装。Kotlin 通过编译器检查和必要的运行时断言共同维护空安全契约。

这意味着空安全不是性能负担很重的机制,而是 API 设计和编译期检查机制。

平台类型

当 Kotlin 调用 Java 代码时,如果 Java 类型没有空性注解,Kotlin 不知道它到底可不可能为 null。这种类型叫平台类型,通常在 IDE 中显示为类似 String! 的形式。

Java:

public class UserApi {
    public String findName(long id) {
        return null;
    }
}

Kotlin:

val name = userApi.findName(1)
println(name.length) // 可能运行时 NPE

平台类型是 Kotlin 空安全的重要边界。编译器会给你灵活性,但也意味着你要自己判断是否需要空值检查。

给 Java API 补充空性注解

Java 代码可以使用 JetBrains 注解、JSpecify、Eclipse 注解等表达空性:

import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

public class UserApi {
    public @Nullable String findName(long id) {
        return null;
    }

    public void save(@NotNull String name) {
    }
}

Kotlin 调用时就能得到更准确的类型:

val name: String? = userApi.findName(1)

Java null-check 到 Kotlin 的迁移

Java:

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

Kotlin 直接迁移:

val order = findOrder()
if (order != null) {
    processCustomer(order.customer)
}

更 Kotlin 的写法:

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

如果代码块较复杂,直接使用 if 也完全可以。Kotlin 风格不是强迫你把所有逻辑都写成链式调用,而是选择最清晰的表达。

默认值迁移

Java:

Order order = findOrder();
if (order == null) {
    order = new Order(new Customer("guest"));
}

Kotlin:

val order = findOrder() ?: Order(Customer("guest"))

Elvis 运算符让“为空时使用默认值”的意图非常直接。

安全获取集合元素

Java:

List<Integer> numbers = List.of(1, 2);
Integer first = numbers.get(0);
// numbers.get(5); // IndexOutOfBoundsException

Kotlin:

val numbers = listOf(1, 2)

println(numbers[0])
println(numbers.getOrNull(5)) // null

getOrNull() 的返回类型是 Int?,调用方必须处理“没有这个元素”的情况。

安全类型转换

Java:

int stringLength(Object value) {
    if (value instanceof String text) {
        return text.length();
    }
    return -1;
}

Kotlin:

fun stringLength(value: Any): Int {
    val text = value as? String
    return text?.length ?: -1
}

as? 转换失败返回 null,比直接 as 抛异常更适合不确定输入。

泛型集合的空性风险

Java 和 Kotlin 共用集合时要格外小心。Java 代码可能向集合里放入 null,即使 Kotlin 侧看到的是 MutableList<String>

val names: MutableList<String> = mutableListOf("Ada")
javaApi.addNull(names)

如果 Java 侧真的加入 null,Kotlin 后续按非空字符串处理时可能出问题。边界层需要防御:

val safeNames = names.filterNotNull()

更好的做法是在 Java API 上补充空性注解,并避免跨语言共享可变集合。

!! 不是迁移方案

从 Java 迁移时很容易为了让代码编译通过写大量 !!

val name = userApi.findName(id)!!

这等于告诉 Kotlin:“如果为空就让它崩。”更好的方式是根据业务语义选择:

val name = userApi.findName(id) ?: return

或:

val name = userApi.findName(id)
    ?: throw IllegalStateException("用户 $id 没有名字")

异常信息应该说明业务上下文,而不是让普通 NPE 暴露到日志中。

迁移建议

  • Java API 能补注解就补注解。
  • Kotlin 边界层尽快把平台类型转成明确的 TT?
  • 避免跨 Java/Kotlin 共享可变集合。
  • 少用 !!,多用 ?: return?: throw?.let {}
  • 不要把所有 Java Optional 机械翻译成 Kotlin 可空类型;领域语义更重要。

参考

  • 官方 Java/Kotlin 空性指南:https://kotlinlang.org/docs/java-to-kotlin-nullability-guide.html
  • 官方空安全文档:https://kotlinlang.org/docs/null-safety.html
  • 官方 Java 互操作文档:https://kotlinlang.org/docs/java-interop.html