Kotlin 20:Java 调用 Kotlin 时默认参数去哪了
Kotlin 可以写 describe("task-1"),让第二个参数使用默认值;Java 调用同一个声明,却不一定能少传参数。反方向的 19已经补过 class、interface、implements、static、泛型擦除和 checked exception 的最小 Java 前置;这次将边界倒过来:用真实 src/test/java 消费者编译调用 Kotlin 方法,比较 @JvmOverloads、@JvmStatic、@Throws 到底改变了 Java 所见的哪一部分契约。
冻结组合仍是 Kotlin Gradle Plugin/stdlib 2.2.21、JDK 21、Gradle 8.14;本篇只生成单个同步 facade,不发布公共库。官方 Java 调 Kotlin 文档提供注解和属性映射规则;下文的具体生成方法、Java 编译诊断和异常结果来自本次 javap、javac 和 JUnit,不能拿滚动文档代替当前产物。
Java 消费者只认识类文件里实际存在的方法、字段访问器和 throws 签名。Kotlin 的 object、属性、默认参数、internal 与接口默认实现都要先落成 JVM 成员,Java 源码才能调用。这里的桥接规则很窄:Java 的 static 调用需要可见的静态方法;Java 的 checked exception 只看方法签名上有没有声明;接口默认方法能不能由 Java 匿名类直接继承,要看当前 Kotlin 编译模式生成的字节码。源码里“看起来有默认值”不等于 Java 编译器已经获得一条少参数入口。
Kotlin 的默认值为什么不等于 Java 的单参数方法
给同一个 TaskId 提供面向 Java 的简单字符串入口,而不是要求 Java 直接使用 16 的值类生成符号:
1 | |
Java 测试可直接调用静态 TaskJavaApi.describe("task-1") 得到 task-1:draft,传两个参数得到 task-1:running;没加 @JvmOverloads 的另一方法传全两个参数也能得到 task-1:running。只调用 describeWithoutOverloads("task-1") 的 Java 源码在单独 javac --release 21 中退出 1,实际报告 required: String,String; found: String。两个 Kotlin 声明都有默认值,区别是第一个声明生成了 Java 源码可直接调用的单参数重载。官方 overload 生成规则是依据,不是“任意默认值自动变成所有 Java 重载”。
@JvmStatic 让 object 上的这组方法具有 Java 静态调用入口;不加它时还需检查具体声明生成的 Java 访问形式,不能把 Kotlin object 和 Java static 仅凭字面名称画等号。这次 javap -p TaskJavaApi 同时列出 describe(String,String) 和 describe(String),而 describeWithoutOverloads 只有公开调用时需要的两个实参形式。输出里还有 $default 名称的编译器生成辅助方法;那不是本文承诺给 Java 消费者的稳定入口,源码消费者测试只调用显式提供的 facade。下次换 Kotlin 版本,名字修饰与 ABI 都要重新核对。
属性、internal 和接口默认方法如何暴露
同一个 TaskJavaApi 还声明了 @get:JvmStatic val category = "tasks"。Java 测试不是猜 Kotlin 属性能否像字段一样访问,而是实测 TaskJavaApi.getCategory() 返回 tasks,javap -p 中也有静态 getCategory();这是当前属性 getter 的访问形状,不是说每个 Kotlin 属性天然都会变成 Java 公共静态字段。
此外定义 internal fun implementationMarker() 仅供本模块内部使用。Java 测试枚举 TaskJavaApi.class.getDeclaredMethods(),找到该方法名的前缀;本次 javap 记录为 implementationMarker$kotlin_task_language(),可见 Kotlin 源级 internal 不等于 JVM 反射不可见。后缀随编译器和模块配置影响,Java API 不应直接依赖这个方法;要隐藏敏感数据或隔离模块必须另做访问/发布边界,不能把 internal 当权限机制。官方 Java 可见性说明用于确认这一语义范围,本机符号以冻结产物为准。
接口侧新增一个只返回 "observed:$id" 的 TaskJavaObserver.describe(id) 默认实现。在本项目当前未显式指定 jvmDefault 选项的编译组合中,javap 实际给出 public default java.lang.String describe(java.lang.String),Java 测试用匿名实现不覆写方法也成功得到 observed:task-1。这只说明本次接口默认方法按该模式生成;换目标或显式修改默认方法模式时必须重新编译真实 Java 消费者。官方 default methods/compatibility modes列的是可选模式,不代表我们已实跑全部模式。
Java 的 checked 异常由哪个声明负责
Kotlin 没有要求调用者必须 catch 的 checked exception 语法,但 Java 消费者需要知道接口可抛什么。用一条明确的合成失败路径做对照:
1 | |
这不是磁盘或数据库真的报错;函数体就是直接抛一个 IOException。javap 的当前签名为 readTask(String) throws java.io.IOException;Java JUnit 测试用 try/catch(IOException) 捕获并核对错误文本。@Throws 的作用是给 Java 可见的方法声明异常类型,不是让 Kotlin 自动打开文件、实现事务回滚,或证明外部操作已经停止。官方 checked exceptions 小节说明了这个注解边界。本例不让 Java 直接拿 Kotlin 的 Result<SubmissionDecision> 当公共异常协议;那类 API 须另测 Java 源码和二进制消费者。
Java 代码即便面对 Kotlin id: String,也能写 TaskJavaApi.describe(null) 并通过本次 javac;真实运行由 Kotlin 方法入口的非空参数检查抛 NullPointerException,Java JUnit 对此有断言。此处的反例呼应 19:往返互操作都需要在信任边界明确空值来源,类文件注解或 IDE 警告不能当 Java 调用者绝不传 null 的运行证明。
TaskId 的签名边界和本次能证明的事
Java facade 的公开参数刻意是 String,实现内部构造 TaskId(id);这方便 Java 消费者按稳定、可测试的形状调用,同时仍在 Kotlin 内执行非空白 ID 校验。上一章的 submitTyped-W6ZOYFM 是当时 Kotlin 2.2.21 的生成方法名,并没有在本章推荐给 Java 侧直接调用。这里由 @JvmOverloads 的真实 Java 编译与执行确定可用重载,由 @Throws 的签名和捕获确定 checked 契约;对未来版本的二进制兼容与发布行为,还需在后续库发布选修 E07 做旧消费者对照。
复跑命令为 bash examples/kotlin-language/verification/20.sh:离线 clean test 中实际执行了 compileKotlin、compileJava、compileTestKotlin、compileTestJava,00–20 二十一组测试各 2/2;另编译的错误 Java 源码获得预期诊断,当前 TaskJavaObserver 的 javap 也独立记录,旧 CLI 正反例保留原始退出码。本篇同名素材目录提供 含Wrapper与全部累计实验的可解包工程(SHA-256校验);旧 CLI 仍未调用 TaskJavaApi,测试调用只发生在 Java JUnit 消费者里。
练习:给 describeWithoutOverloads 加了 @JvmStatic,Java 是否就能调用一参数版本?不能。本次已经是静态调用入口,却在 javac 报缺第二参数;是否生成重载由 @JvmOverloads 决定。把 @Throws(IOException::class) 移除后 Java 是否还可能在运行时碰到 IOException?Java 的检查异常签名与 Kotlin 函数运行时抛出是两回事;移除注解会改变 Java 编译期契约,但不能据此断言函数体不会抛异常。本章未实际运行删除注解的版本,不报其诊断为已验收。
[PATTERN] 用 Java 消费者验证 Kotlin 对外 API,分别检查能否编译期调用、生成什么 JVM 签名、运行时返回或抛什么。这三个问题分别由 Java 源码、javap、JUnit 回答;看见源码默认值或某个 $default 辅助符号,不能省略其中任何一项。
| 声明与调用 | 本篇结果 | 不代表 |
|---|---|---|
@JvmStatic @JvmOverloads describe |
Java 单参/双参均编译运行 | 所有默认参数自动有重载 |
describeWithoutOverloads |
Java 双参成功、单参 javac 拒绝 |
$default 是稳定公共 API |
属性 getter / internal / 接口默认方法 |
Java 静态 getter 可调、内部名字可见、当前接口 default 可运行 |
任意 JVM 编译模式都相同,或 internal 是安全隔离 |
@Throws(IOException::class) readTask |
Java 看到 throws 并捕获合成异常 |
真实 I/O 已执行或事务已回滚 |
下一篇检查 Kotlin Gradle 构建、测试与依赖版本怎样决定“工程能被复跑”,不会把一次 Java JUnit 通过扩展成所有 Java 库的兼容性证明。






