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) 可以使用 lambda 语法在 Kotlin 和 Java 中实现,这以惯用的方式内联实现。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