Kotlin/JVM 与 Java Records

Kotlin/JVM 运行在 JVM 上,可以自然调用 Java,也可以生成给 Java 使用的字节码。本章聚焦几个容易影响 Java 互操作体验的 JVM 主题:Java records、Kotlin 数据类、@JvmRecord、接口默认方法和 JVM API 设计。

Java records 是什么

Java record 是用于保存不可变数据的类:

public record Person(String name, int age) {}

Java 编译器会生成:

  • 私有 final 字段。
  • 全参数构造函数。
  • record component 访问方法,例如 name()age()
  • equals()hashCode()toString()

它和 Kotlin data class 很像,但不是同一个概念。

在 Kotlin 中使用 Java record

Java:

public record Person(String name, int age) {}

Kotlin:

val person = Person("Ada", 36)
println(person.name)
println(person.age)

Kotlin 可以像访问属性一样访问 record component。

Kotlin data class 与 Java record 对比

Kotlin:

data class Person(val name: String, val age: Int)

Java record:

public record Person(String name, int age) {}

相似点:

  • 都适合表达数据载体。
  • 都自动生成结构相等相关方法。
  • 都减少样板代码。

差异:

  • Kotlin 数据类有 copy() 和解构。
  • Java record 访问器是 name(),不是 getName()
  • Java record 隐式继承 java.lang.Record
  • Kotlin data class 默认不是 JVM record。
  • @JvmRecord 会改变生成字节码和 Java API 形态。

在 Kotlin 中声明 JVM record

@JvmRecord
data class Person(val name: String, val age: Int)

这会让 Kotlin data class 按 JVM record 形式生成 class 文件。

注意:给已有类添加 @JvmRecord 不是二进制兼容变更,因为属性访问器命名等字节码形态会改变。

@JvmRecord 要求

使用 @JvmRecord 的 data class 必须满足一些要求:

  • 模块目标 JVM bytecode 版本至少是 16。
  • 类不能显式继承其他类,因为 JVM record 隐式继承 java.lang.Record
  • 可以实现接口。
  • 不能声明带 backing field 的额外属性,除非属性来自主构造函数参数。
  • 不能有带 backing field 的可变属性。
  • 不能是局部类。
  • 主构造函数可见性必须和类本身一致。

这意味着 @JvmRecord 适合很纯粹的数据载体,不适合复杂业务对象。

注解与 record component

Java record 的注解可能传播到 record component、字段、访问器和构造参数。Kotlin 可以通过 @all: 接近这种行为:

@JvmRecord
data class Person(
    val name: String,
    @all:Positive val age: Int
)

如果注解需要同时适配 Kotlin property 和 Java record component,自定义注解时要设置合适的 Kotlin @Target 和 Java @java.lang.annotation.Target

什么时候用 @JvmRecord

适合:

  • 你的 Kotlin 类型主要服务 Java 16+ 调用方。
  • API 明确需要 Java record 语义。
  • 数据模型非常简单、不可变、无额外状态。
  • 与 Java record 生态工具互操作。

不适合:

  • 纯 Kotlin 内部模型。
  • Android 或低 JVM target 项目。
  • 需要 var、额外 backing field、复杂初始化状态。
  • 不想破坏已有 Java/Kotlin 二进制兼容。

如果只是 Kotlin 内部代码,普通 data class 通常更自然。

JVM 接口默认方法

Kotlin 接口可以有默认实现:

interface Robot {
    fun move() {
        println("walking")
    }

    fun speak()
}

JVM 上,Kotlin 可以把接口函数编译成 Java default methods。编译行为受 -jvm-default 相关配置影响,例如是否生成兼容桥和 DefaultImpls

对新项目来说,默认配置通常足够。公共库需要关注二进制兼容,尤其是库升级时 Java 调用方和旧 Kotlin 调用方是否仍能链接。

Kotlin API 给 Java 用时的设计清单

如果 Kotlin/JVM 代码是公共库或被大量 Java 代码调用,至少检查:

  • 顶层函数生成类名是否需要 @file:JvmName
  • 伴生对象方法是否需要 @JvmStatic
  • 默认参数是否需要 @JvmOverloads
  • 属性是否应该暴露 getter/setter,还是 @JvmField
  • checked exception 是否需要 @Throws
  • 泛型通配符是否需要 @JvmWildcard@JvmSuppressWildcards
  • inline value class 是否需要 Java 友好的替代 API。
  • data class 是否真的需要 @JvmRecord

Java 调用体验示例

Kotlin:

@file:JvmName("Users")

package demo

data class User(val id: Long, val name: String)

@JvmOverloads
fun createUser(id: Long, name: String = "unknown"): User =
    User(id, name)

Java:

User a = Users.createUser(1L);
User b = Users.createUser(2L, "Ada");

如果没有 @file:JvmName,Java 会看到文件名加 Kt 的类名;如果没有 @JvmOverloads,Java 只能调用完整参数版本。

实践建议

  • Kotlin 内部模型优先使用普通 data class,不要为了“像 Java”主动 @JvmRecord
  • 面向 Java 16+ record 生态时再使用 @JvmRecord
  • 添加 @JvmRecord 前评估二进制兼容影响。
  • 公共 Kotlin/JVM API 必须从 Java 侧写调用样例验证。
  • 接口默认方法和泛型通配符属于库作者必须关注的 ABI 细节。

参考

  • 官方 Java records 文档:https://kotlinlang.org/docs/jvm-records.html
  • 官方 Java 调 Kotlin 文档:https://kotlinlang.org/docs/java-to-kotlin-interop.html