Kotlin 与 Java 互操作指南

本文档是一套用于在 Java 和 Kotlin 中编写公共 API 的规则,旨在使代码在从另一种语言使用时感觉习惯。

上次更新:2024-07-29

Java(供 Kotlin 使用)

无硬关键字

不要使用任何 Kotlin 的 硬关键字 作为方法或字段的名称。在从 Kotlin 调用时,这些需要使用反引号进行转义。 软关键字修饰符关键字特殊标识符 是允许的。

例如,Mockito 的 when 函数在从 Kotlin 使用时需要反引号

val callable = Mockito.mock(Callable::class.java)
Mockito.`when`(callable.call()).thenReturn(/* … */)

避免 Any 扩展名

除非绝对必要,否则避免对方法使用 Any 上的扩展函数 的名称,或对字段使用 Any 上的扩展属性 的名称。虽然成员方法和字段将始终优先于 Any 的扩展函数或属性,但在阅读代码时可能难以知道正在调用哪个。

可空性注释

公共 API 中的每个非原始参数、返回值和字段类型都应具有可空性注释。未注释的类型被解释为 “平台”类型,其可空性不明确。

默认情况下,Kotlin 编译器标志会尊重 JSR 305 注释,但会用警告标记它们。您还可以设置一个标志,使编译器将注释视为错误。

Lambda 参数最后

有资格进行 SAM 转换 的参数类型应放在最后。

例如,RxJava 2 的 Flowable.create() 方法签名定义为

public static <T> Flowable<T> create(
    FlowableOnSubscribe<T> source,
    BackpressureStrategy mode) { /* … */ }

因为 FlowableOnSubscribe 有资格进行 SAM 转换,所以从 Kotlin 调用此方法的函数看起来像这样

Flowable.create({ /* … */ }, BackpressureStrategy.LATEST)

但是,如果方法签名中的参数反转,则函数调用可以使用尾随 lambda 语法

Flowable.create(BackpressureStrategy.LATEST) { /* … */ }

属性前缀

要将方法表示为 Kotlin 中的属性,必须使用严格的“bean”样式前缀。

访问器方法需要 get 前缀,或者对于返回布尔值的的方法,可以使用 is 前缀。

public final class User {
  public String getName() { /* … */ }
  public boolean isActive() { /* … */ }
}
val name = user.name // Invokes user.getName()
val active = user.isActive // Invokes user.isActive()

关联的变异器方法需要 set 前缀。

public final class User {
  public String getName() { /* … */ }
  public void setName(String name) { /* … */ }
  public boolean isActive() { /* … */ }
  public void setActive(boolean active) { /* … */ }
}
user.name = "Bob" // Invokes user.setName(String)
user.isActive = true // Invokes user.setActive(boolean)

如果您希望将方法公开为属性,请不要使用非标准前缀,例如 hasset 或非 get 前缀的访问器。具有非标准前缀的方法仍然可以作为函数调用,这在取决于方法行为的情况下可能是可以接受的。

运算符重载

请注意允许特殊调用站点语法(例如 Kotlin 中的 运算符重载)的方法名称。确保此类方法名称与简写语法一起使用时有意义。

public final class IntBox {
  private final int value;
  public IntBox(int value) {
    this.value = value;
  }
  public IntBox plus(IntBox other) {
    return new IntBox(value + other.value);
  }
}
val one = IntBox(1)
val two = IntBox(2)
val three = one + two // Invokes one.plus(two)

Kotlin(供 Java 使用)

文件名

当文件包含顶级函数或属性时,始终使用 @file:JvmName("Foo") 对其进行注释以提供一个好听的名称。

默认情况下,文件 MyClass.kt 中的顶级成员将最终位于名为 MyClassKt 的类中,这很不吸引人,并且会泄露语言作为实现细节。

考虑添加 @file:JvmMultifileClass 以将来自多个文件中的顶级成员组合到一个类中。

Lambda 参数

在 Java 中定义的单方法接口 (SAM) 可以在 Kotlin 和 Java 中使用 lambda 语法实现,这以惯用的方式内联实现。Kotlin 有几个定义此类接口的选项,每个选项都有细微的差别。

首选定义

旨在从 Java 中使用的高阶函数 不应该使用返回Unit函数类型,因为这将要求 Java 调用者返回Unit.INSTANCE。不要在签名中内联函数类型,而是使用函数式(SAM)接口。此外,在定义预期用作 lambda 的接口时,也考虑使用函数式(SAM)接口 而不是普通接口,这允许从 Kotlin 进行惯用的使用。

考虑以下 Kotlin 定义

fun interface GreeterCallback {
  fun greetName(String name)
}

fun sayHi(greeter: GreeterCallback) = /* … */

从 Kotlin 调用时

sayHi { println("Hello, $it!") }

从 Java 调用时

sayHi(name -> System.out.println("Hello, " + name + "!"));

即使函数类型没有返回Unit,将其设为命名接口也可能是一个好主意,以便调用者可以使用命名类而不是仅使用 lambda 来实现它(在 Kotlin 和 Java 中)。

class MyGreeterCallback : GreeterCallback {
  override fun greetName(name: String) {
    println("Hello, $name!");
  }
}

避免返回Unit的函数类型

考虑以下 Kotlin 定义

fun sayHi(greeter: (String) -> Unit) = /* … */

它要求 Java 调用者返回Unit.INSTANCE

sayHi(name -> {
  System.out.println("Hello, " + name + "!");
  return Unit.INSTANCE;
});

当实现旨在具有状态时,避免使用函数式接口

当接口实现旨在具有状态时,使用 lambda 语法没有意义。Comparable 就是一个突出的例子,因为它旨在将thisother进行比较,而 lambda 没有this。不为接口添加fun前缀会强制调用者使用object : ...语法,这允许它具有状态,从而为调用者提供提示。

考虑以下 Kotlin 定义

// No "fun" prefix.
interface Counter {
  fun increment()
}

它阻止了 Kotlin 中的 lambda 语法,需要使用这种较长的版本

runCounter(object : Counter {
  private var increments = 0 // State

  override fun increment() {
    increments++
  }
})

避免Nothing泛型

其泛型参数为Nothing的类型会对 Java 公开为原始类型。原始类型在 Java 中很少使用,应避免使用。

记录异常

可能抛出已检查异常的函数应使用@Throws对其进行记录。运行时异常应在 KDoc 中记录。

注意函数委托到的 API,因为它们可能会抛出已检查异常,而 Kotlin 否则会静默地允许其传播。

防御性复制

从公共 API 返回共享或未拥有的只读集合时,请将其包装在不可修改的容器中或执行防御性复制。尽管 Kotlin 强制执行其只读属性,但在 Java 端没有这样的强制执行。如果没有包装器或防御性复制,则可以通过返回长期存在的集合引用来违反不变性。

伴随对象函数

**伴随对象**中的公共函数必须使用@JvmStatic注释才能公开为静态方法。

如果没有此注释,这些函数只能作为静态Companion字段上的实例方法使用。

错误:没有注释

class KotlinClass {
    companion object {
        fun doWork() {
            /* … */
        }
    }
}
public final class JavaClass {
    public static void main(String... args) {
        KotlinClass.Companion.doWork();
    }
}

正确:@JvmStatic注释

class KotlinClass {
    companion object {
        @JvmStatic fun doWork() {
            /* … */
        }
    }
}
public final class JavaClass {
    public static void main(String... args) {
        KotlinClass.doWork();
    }
}

伴随对象常量

companion object中,公共的、非const属性,如果实际上是常量,则必须使用@JvmField注释才能公开为静态字段。

如果没有此注释,这些属性只能作为静态Companion字段上名称奇怪的实例“getter”使用。使用@JvmStatic而不是@JvmField会将名称奇怪的“getter”移动到类上的静态方法,但这仍然不正确。

错误:没有注释

class KotlinClass {
    companion object {
        const val INTEGER_ONE = 1
        val BIG_INTEGER_ONE = BigInteger.ONE
    }
}
public final class JavaClass {
    public static void main(String... args) {
        System.out.println(KotlinClass.INTEGER_ONE);
        System.out.println(KotlinClass.Companion.getBIG_INTEGER_ONE());
    }
}

错误:@JvmStatic注释

class KotlinClass {
    companion object {
        const val INTEGER_ONE = 1
        @JvmStatic val BIG_INTEGER_ONE = BigInteger.ONE
    }
}
public final class JavaClass {
    public static void main(String... args) {
        System.out.println(KotlinClass.INTEGER_ONE);
        System.out.println(KotlinClass.getBIG_INTEGER_ONE());
    }
}

正确:@JvmField注释

class KotlinClass {
    companion object {
        const val INTEGER_ONE = 1
        @JvmField val BIG_INTEGER_ONE = BigInteger.ONE
    }
}
public final class JavaClass {
    public static void main(String... args) {
        System.out.println(KotlinClass.INTEGER_ONE);
        System.out.println(KotlinClass.BIG_INTEGER_ONE);
    }
}

惯用命名

Kotlin 与 Java 的调用约定不同,这可能会改变你命名函数的方式。使用@JvmName设计名称,使其在两种语言的约定中都感觉惯用,或者使其与各自的标准库命名相匹配。

这最常发生在扩展函数和扩展属性上,因为接收器类型的放置位置不同。

sealed class Optional<T : Any>
data class Some<T : Any>(val value: T): Optional<T>()
object None : Optional<Nothing>()

@JvmName("ofNullable")
fun <T> T?.asOptional() = if (this == null) None else Some(this)
// FROM KOTLIN:
fun main(vararg args: String) {
    val nullableString: String? = "foo"
    val optionalString = nullableString.asOptional()
}
// FROM JAVA:
public static void main(String... args) {
    String nullableString = "Foo";
    Optional<String> optionalString =
          Optionals.ofNullable(nullableString);
}

用于默认值的功能重载

具有带有默认值的参数的函数必须使用@JvmOverloads。如果没有此注释,则无法使用任何默认值来调用该函数。

当使用@JvmOverloads时,请检查生成的每个方法是否有意义。如果没有,请执行以下一项或两项重构操作,直到满意为止

  • 更改参数顺序,优先考虑将具有默认值的那些放在末尾。
  • 将默认值移动到手动函数重载中。

错误:没有@JvmOverloads

class Greeting {
    fun sayHello(prefix: String = "Mr.", name: String) {
        println("Hello, $prefix $name")
    }
}
public class JavaClass {
    public static void main(String... args) {
        Greeting greeting = new Greeting();
        greeting.sayHello("Mr.", "Bob");
    }
}

正确:@JvmOverloads注释。

class Greeting {
    @JvmOverloads
    fun sayHello(prefix: String = "Mr.", name: String) {
        println("Hello, $prefix $name")
    }
}
public class JavaClass {
    public static void main(String... args) {
        Greeting greeting = new Greeting();
        greeting.sayHello("Bob");
    }
}

Lint 检查

要求

  • Android Studio 版本:3.2 Canary 10 或更高版本
  • Android Gradle 插件版本:3.2 或更高版本

支持的检查

现在有 Android Lint 检查可以帮助你检测和标记之前描述的一些互操作性问题。仅检测 Java 中的问题(供 Kotlin 使用)。具体来说,支持的检查是

  • 未知空性
  • 属性访问
  • 没有硬编码 Kotlin 关键字
  • Lambda 参数放在最后

Android Studio

要启用这些检查,请转到**文件 > 首选项 > 编辑器 > 检查**,并在 Kotlin 互操作性下选中要启用的规则

图 1. Android Studio 中的 Kotlin 互操作性设置。

选中要启用的规则后,在你运行代码检查(**分析 > 检查代码…**)时,新的检查将运行。

命令行构建

要从命令行构建中启用这些检查,请在你的build.gradle文件中添加以下行

Groovy

android {

    ...

    lintOptions {
        enable 'Interoperability'
    }
}

Kotlin

android {
    ...

    lintOptions {
        enable("Interoperability")
    }
}

有关在 lintOptions 内部支持的完整配置集,请参阅 Android 的Gradle DSL 参考

然后,从命令行运行./gradlew lint