21、Kotlin与Java互操作:Kotlin调用Java、Java调用Kotlin、@JvmStatic与@JvmOverloads、平台类型

Kotlin 和 Java 能无缝互调,这是它最大的杀手锏之一。说白了,你可以在一个项目里同时用两种语言,老代码不用重写,新功能用 Kotlin 写,慢慢过渡。我在好几个遗留项目里都这么干过,效果非常好。

但互操作不是完全无痛的。有些坑你得提前知道,比如空安全怎么处理、静态方法怎么暴露、重载函数怎么生成。今天我们就把这些关键点一一讲透。

Kotlin 调用 Java:比你想象的更简单

Kotlin 调用 Java 代码,基本是零成本的。你直接写 Java 类的全限定名,或者直接 import 进来,就能用。Kotlin 编译器会自动把 Java 的 getter/setter 映射成 Kotlin 的属性语法。

// Java 类
public class User {
    private String name;
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
}

// Kotlin 中调用
val user = User()
user.name = "Kotlin"  // 实际上调用了 setName()
println(user.name)    // 实际上调用了 getName()

我个人习惯是,只要 Java 类遵循标准的 JavaBean 规范,Kotlin 里就能直接用属性语法,非常舒服。

小提示: 如果 Java 类的方法名不符合 get/set 规范,Kotlin 就不会自动映射成属性。这时候你只能老老实实调用方法。

Java 调用 Kotlin:需要一点额外工作

反过来,Java 调用 Kotlin 代码,情况就稍微复杂一点。因为 Kotlin 有一些 Java 没有的特性,比如顶层函数、扩展函数、默认参数、空安全等。这些特性在 Java 看来,需要一些「桥接」机制。

顶层函数与 @JvmStatic

Kotlin 里你可以直接在文件里写函数,不放在类里。这在 Java 看来是什么?Kotlin 编译器会生成一个「文件类」,函数变成这个类的静态方法。

// Kotlin 顶层函数,文件名为 Utils.kt
fun sayHello(name: String) = "Hello, $name"

// Java 中调用
String result = UtilsKt.sayHello("Java");

类名是文件名 + "Kt" 后缀。说实话,这个命名规则有点丑。我在项目中一般用 @file:JvmName 注解来指定一个更优雅的类名。

// Kotlin 文件顶部
@file:JvmName("StringUtils")
fun sayHello(name: String) = "Hello, $name"

// Java 中调用
String result = StringUtils.sayHello("Java");

@JvmStatic 是干嘛用的?它用在 Kotlin 的 companion object 里。默认情况下,companion object 里的方法在 Java 看来是「实例方法」——你得先拿到 companion 对象才能调用。加上 @JvmStatic 后,它就变成真正的静态方法了。

class MyClass {
    companion object {
        @JvmStatic
        fun create(): MyClass = MyClass()
    }
}

// Java 中调用
MyClass.create();  // 直接调用,不需要 MyClass.Companion.create()
重点: 如果你在 Java 中频繁调用 Kotlin 的 companion 方法,强烈建议加上 @JvmStatic。否则每次都要写 .Companion,代码又丑又啰嗦。

默认参数与 @JvmOverloads

Kotlin 支持函数默认参数,但 Java 不支持。如果你在 Kotlin 里写了一个带默认参数的函数,Java 调用时就必须把所有参数都传进去。这显然不合理。

解决方案就是 @JvmOverloads。这个注解会让 Kotlin 编译器生成多个重载版本,每个版本依次去掉最后一个参数。

class Dialog {
    @JvmOverloads
    fun show(title: String, message: String = "", cancelable: Boolean = true) {
        // ...
    }
}

// Java 中调用
Dialog dialog = new Dialog();
dialog.show("提示");                    // 自动生成
dialog.show("提示", "内容");            // 自动生成
dialog.show("提示", "内容", false);     // 自动生成

我曾经在一个 Android 项目里,因为忘了加 @JvmOverloads,导致 Java 那边的同事一直抱怨「为什么 Kotlin 的函数参数这么多」。加上之后,世界清净了。

注意: @JvmOverloads 不能用于构造函数?其实可以。在 Kotlin 的 data class 或普通类的构造函数上也能用。但要注意,如果参数太多,生成的 overloads 数量会爆炸,建议控制在 3-4 个以内。

平台类型:空安全的灰色地带

这是互操作里最让人头疼的问题。Kotlin 有严格的空安全:String? 可空,String 不可空。但 Java 没有这个区分。那从 Java 过来的类型,到底是可空还是不可空?

Kotlin 的处理方式是:引入「平台类型」(Platform Type)。平台类型用 String! 表示,意思是「我也不知道能不能为空,你自己看着办」。

// Java 方法
public String getName() { return name; }

// Kotlin 中调用
val name: String = getName()  // 编译通过,但运行时可能 NPE
val name2: String? = getName() // 也可以

你可以把它当成不可空类型用,也可以当成可空类型用。但如果你选错了,运行时就会崩。我记得有一次线上 bug,就是因为一个 Java 方法返回了 null,而我在 Kotlin 里把它当成了非空类型,直接调用方法,结果 NPE。

避坑指南: 我曾经在接手一个混合项目时,把所有从 Java 来的返回值都声明为可空类型(加上 ?),然后逐个确认。虽然麻烦,但安全。等确认无误后,再改成非空。

平台类型只在 Kotlin 中可见。当你把 Kotlin 代码编译成字节码后,平台类型就消失了,变成了普通的 Java 类型。所以这个「灰色地带」只在写 Kotlin 代码时存在。

知识体系总览

下面这张图帮你理清 Kotlin 与 Java 互操作的核心脉络:

Kotlin 与 Java 互操作核心知识体系 Kotlin 调用 Java • 直接使用 Java 类,import 即可 • getter/setter 自动映射为属性 • 平台类型:String! 需手动处理空安全 Java 调用 Kotlin • 顶层函数 → 文件类静态方法 • @JvmStatic → 真正的静态方法 • @JvmOverloads → 生成重载版本 核心注解与工具 • @file:JvmName → 自定义文件类名 • @JvmStatic → companion 方法变静态 • @JvmOverloads → 生成多个重载 常见陷阱与避坑 • 平台类型 NPE:Java 返回 null 时 • 忘记 @JvmStatic → 调用链变长 • 默认参数无 @JvmOverloads → 必须全传 核心原则:Java 代码尽量保持规范(JavaBean、非空返回),Kotlin 代码主动添加互操作注解

实战建议

说了这么多,总结几条我在实际项目中的经验:

  • Java 代码尽量保持规范:方法名符合 get/set 规范,返回值尽量不返回 null。这样 Kotlin 调用时最省心。
  • Kotlin 代码主动加注解:如果你知道这个 Kotlin 类会被 Java 调用,提前加上 @JvmStatic@JvmOverloads。别等 Java 同事来抱怨。
  • 平台类型要警惕:从 Java 来的返回值,先当成可空类型处理。等确认不会返回 null 后,再改成非空。安全第一。
  • 使用 @file:JvmName:给顶层函数所在的文件起个好名字,别用默认的 "XxxKt"。

互操作是 Kotlin 的强项,但也是容易出问题的地方。你只要记住这几个关键点,就能在混合项目中游刃有余。

我的习惯: 每次新建 Kotlin 文件时,如果里面包含顶层函数或 companion object,我会先问自己一句:「这个会被 Java 调用吗?」如果答案是「会」,立刻加上对应的注解。省得后面再改。

公众号:蓝海资料掘金营,微信deep3321