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 调用 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()
默认参数与 @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 的函数参数这么多」。加上之后,世界清净了。
平台类型:空安全的灰色地带
这是互操作里最让人头疼的问题。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。
平台类型只在 Kotlin 中可见。当你把 Kotlin 代码编译成字节码后,平台类型就消失了,变成了普通的 Java 类型。所以这个「灰色地带」只在写 Kotlin 代码时存在。
知识体系总览
下面这张图帮你理清 Kotlin 与 Java 互操作的核心脉络:
实战建议
说了这么多,总结几条我在实际项目中的经验:
- Java 代码尽量保持规范:方法名符合 get/set 规范,返回值尽量不返回 null。这样 Kotlin 调用时最省心。
- Kotlin 代码主动加注解:如果你知道这个 Kotlin 类会被 Java 调用,提前加上
@JvmStatic和@JvmOverloads。别等 Java 同事来抱怨。 - 平台类型要警惕:从 Java 来的返回值,先当成可空类型处理。等确认不会返回 null 后,再改成非空。安全第一。
- 使用 @file:JvmName:给顶层函数所在的文件起个好名字,别用默认的 "XxxKt"。
互操作是 Kotlin 的强项,但也是容易出问题的地方。你只要记住这几个关键点,就能在混合项目中游刃有余。
公众号:蓝海资料掘金营,微信deep3321