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
}
运行时没有“可空包装”¶
String 和 String? 的差异主要存在于编译期类型系统。运行时对象本身并不会因为 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 边界层尽快把平台类型转成明确的
T或T?。 - 避免跨 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