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();
    }
}

伴生常量

伴生对象中作为有效常量的公共非 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

要启用这些检查,请前往 File > Preferences > Editor > Inspections,然后在 Kotlin Interoperability 下勾选您要启用的规则

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

勾选您要启用的规则后,当您运行代码检查时(Analyze > Inspect Code…),新的检查将运行。

命令行构建

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

Groovy

android {

    ...

    lintOptions {
        enable 'Interoperability'
    }
}

Kotlin

android {
    ...

    lintOptions {
        enable("Interoperability")
    }
}

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

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