在 Java 中调用 Kotlin

Kotlin 调 Java 通常很自然;Java 调 Kotlin 也可行,但需要理解 Kotlin 编译到 JVM 后的形态。公共 API 如果要被 Java 使用,应主动设计 Java 调用体验,而不是只看 Kotlin 代码是否优雅。

Kotlin 属性在 Java 中的形态

Kotlin:

class User(var firstName: String, val id: Long)

Java 大致看到:

User user = new User("Ada", 1L);
String name = user.getFirstName();
user.setFirstName("Grace");
long id = user.getId();

val 只有 getter,var 有 getter 和 setter。

如果属性名以 is 开头:

class Door(var isOpen: Boolean)

Java:

door.isOpen();
door.setOpen(false);

顶层函数

Kotlin:

// File: Strings.kt
package demo

fun normalize(value: String): String = value.trim()

Java 调用:

String result = demo.StringsKt.normalize("  Ada  ");

默认生成的 Java 类名是文件名加 Kt

可以用 @file:JvmName 改名:

@file:JvmName("StringUtils")

package demo

fun normalize(value: String): String = value.trim()

Java:

String result = demo.StringUtils.normalize("  Ada  ");

多个文件也可以通过 @JvmMultifileClass 合并成同一个 Java facade,但公共库中要谨慎使用,避免二进制兼容和命名混乱。

@JvmField

默认情况下,Kotlin 属性对 Java 暴露为 getter/setter。如果希望 Java 直接访问字段:

class User(id: String) {
    @JvmField
    val ID = id
}

Java:

String id = user.ID;

@JvmField 适合常量式、互操作要求明确的字段。普通业务属性仍推荐通过访问器暴露。

const val

const val MAX_RETRY = 3

Java 中会看到静态字段:

int retry = ConstantsKt.MAX_RETRY;

const val 会被编译期内联。公共库变更常量值时,Java/Kotlin 调用方可能需要重新编译才能看到新值。

伴生对象与 @JvmStatic

Kotlin:

class User private constructor(val name: String) {
    companion object {
        fun create(name: String): User = User(name)
    }
}

Java 默认调用:

User user = User.Companion.create("Ada");

如果希望 Java 像调用静态方法一样调用:

class User private constructor(val name: String) {
    companion object {
        @JvmStatic
        fun create(name: String): User = User(name)
    }
}

Java:

User user = User.create("Ada");

object 中的方法同理。@JvmStatic 会额外生成静态方法,同时保留对象实例方法。

默认参数与 @JvmOverloads

Kotlin:

class Circle(
    val x: Int,
    val y: Int,
    val radius: Double = 1.0
)

Kotlin 调用可以省略默认参数:

Circle(0, 0)

Java 默认看不到这种省略能力。需要生成重载时:

class Circle @JvmOverloads constructor(
    val x: Int,
    val y: Int,
    val radius: Double = 1.0
)

方法也可以:

@JvmOverloads
fun draw(label: String, lineWidth: Int = 1, color: String = "red") {
}

注意:@JvmOverloads 会为默认参数生成多个重载,公共 API 中要控制参数数量,避免生成过多方法。

@Throws

Kotlin 没有 checked exception,所以默认不会在 JVM 方法签名上声明 throws

fun writeToFile() {
    throw IOException()
}

Java 无法按 checked exception 方式捕获:

try {
    ExampleKt.writeToFile();
} catch (IOException e) { // 可能编译报错
}

需要时使用:

@Throws(IOException::class)
fun writeToFile() {
    throw IOException()
}

这样 Java 调用方能看到 throws IOException

internal 在 Java 中的可见性

Kotlin 的 internal 在 JVM 字节码层面通常仍是 public,只是会做名称修饰,降低 Java 误用概率。

这意味着:

  • internal 是 Kotlin 编译器层面的模块边界。
  • Java 代码仍可能看到或调用某些 internal 成员。
  • 不要把 internal 当作安全边界。

公共库如果需要真正隐藏实现,要结合模块化、包组织、构建发布和 Java 可见性一起考虑。

泛型通配符

Kotlin 的 out / in 会映射到 Java 通配符:

class Box<out T>(val value: T)

Java 可能看到 Box<? extends Base>。如果生成的通配符影响 Java 调用体验,可以使用:

  • @JvmWildcard
  • @JvmSuppressWildcards

这属于公共 API 设计细节。一般业务模块不需要主动使用,库代码才需要重点关注。

空安全

Java 可以向 Kotlin 非空参数传 null

fun greet(name: String) {
    println(name.length)
}

Java:

ExampleKt.greet(null);

Kotlin 会为公共非空参数生成运行时检查,让问题尽早以异常形式暴露。不要误以为 Java 调用方也能享受 Kotlin 编译期空安全。

实践建议

  • 要被 Java 调用的 Kotlin API,应专门从 Java 写一段调用代码检查体验。
  • 顶层函数公共暴露时考虑 @file:JvmName
  • 伴生对象工厂方法给 Java 用时考虑 @JvmStatic
  • 默认参数给 Java 用时考虑 @JvmOverloads,但不要滥用。
  • checked exception 面向 Java 调用方时使用 @Throws
  • internal 不是 Java 层面的强封装。

参考

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