1. 概述
Kotlin 是一门具备空安全特性的编程语言,其核心设计之一是明确区分可空(nullable)与非可空(non-nullable)类型。
本文将探讨当一个 null
值被赋给非空类型时会发生什么。虽然编译器能在编译期拦截大部分此类问题,但在某些场景下,这类错误会逃过编译检查,在运行时才暴露出来——而这正是我们今天要重点分析的“坑”。
2. 将 null 赋值给非空类型
Kotlin 编译器会在编译期强制执行空安全规则。例如下面这段代码:
val name: String = null
直接编译就会报错:
Kotlin: Null can not be a value of a non-null type String
✅ 编译期防护很到位,基本杜绝了显式的 null
赋值。
❌ 但问题在于:有些 null
赋值在编译期是无法察觉的,比如通过反射、JSON 反序列化、跨语言调用(如 Java 调用 Kotlin)等手段绕过类型系统时。
⚠️ 此时即便变量声明为非空类型,仍可能被注入 null
,最终触发运行时异常。
实例演示:反射导致的 NPE
我们定义一个简单的数据类:
data class User(val id: Long, val username: String)
username
是非空 String
类型,按理说不能为 null
。但如果我们使用反射强行传入 null
:
val constructor = User::class.java.constructors[0]
val createInstance = { constructor.newInstance(42L, null) }
运行时会抛出异常:
val exception = assertThrows { createInstance() }
assertThat(exception.cause).isInstanceOf(NullPointerException::class.java)
assertThat(exception.cause?.message).startsWith("Parameter specified as non-null is null")
如果不捕获异常,控制台输出如下堆栈:
java.lang.NullPointerException: Parameter specified as non-null is null: method User.<init>, parameter username
// 其他堆栈信息...
🔍 关键信息解析:
User.<init>
:表示发生在User
类的构造方法中parameter username
:具体是username
参数被传入了null
- 异常类型是
NullPointerException
,不是IllegalArgumentException
💡 小知识:在 Kotlin 1.4 之前,这种错误抛出的是
IllegalArgumentException
,从 1.4 开始统一改为NullPointerException
,消息格式也更清晰,便于排查。
为什么会这样?
Kotlin 编译器为了保障运行时的空安全,在生成字节码时会对每个非空参数插入空值检查(null check)。大致相当于自动生成了类似这样的代码:
public User(long id, @NotNull String username) {
if (username == null) {
throw new NullPointerException("Parameter 'username' is null");
}
// 正常初始化
}
所以哪怕你通过反射或其他方式绕过了编译器检查,JVM 运行时依然会被这个内置检查拦住。
哪些场景容易踩坑?
以下是几个典型的高危场景,建议重点关注:
- ✅ JSON 反序列化:如 Jackson/Gson 解析 JSON 时字段缺失或为
null
,映射到非空属性上 - ✅ Spring MVC 接口参数绑定:前端传参遗漏,后端 Kotlin 控制器接收非空参数
- ✅ Java 调用 Kotlin 方法:Java 代码传
null
给 Kotlin 的非空参数 - ✅ ORM 框架映射:数据库查出
NULL
字段映射到非空实体属性 - ✅ 反射调用构造函数或方法
如何避免?
场景 | 建议方案 |
---|---|
JSON 反序列化 | 使用 kotlinx.serialization 或配置 Jackson 支持 Kotlin 空安全 |
Spring Boot | 添加 spring.jackson.default-property-inclusion=NON_NULL 并合理使用默认值或可空类型 |
接口参数 | 对外接口尽量使用 String? + 校验逻辑,内部再转为非空 |
数据库映射 | Entity 字段根据实际情况设为可空,或确保查询结果不包含 null |
3. 总结
Kotlin 的空安全不仅体现在编译期,也延伸到了运行时。当你看到如下异常:
java.lang.NullPointerException: Parameter specified as non-null is null
这意味着:某个本应非空的参数或属性,在运行时被赋予了 null
值,而这是 Kotlin 主动抛出的保护机制。
📌 核心要点:
- 编译器能挡住大部分
null
风险 - 反射、序列化、跨语言调用可能绕过编译检查
- Kotlin 在字节码层面插入了运行时空检
- 错误消息精准定位到类、方法和参数名
- 升级到 Kotlin 1.4+ 可获得更友好的异常提示
所有示例代码均可在 GitHub 获取:https://github.com/Baeldung/kotlin-tutorials/tree/master/core-kotlin-modules/core-kotlin-2