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


原始标题:Kotlin NullPointerException: Parameter specified as non-null is null