Kotlin-Java 互操作指南

本文档旨在为 Java 和 Kotlin 的公共 API 编写提供一套规则,以确保代码在被另一种语言调用时具有符合惯用法的体验。

最后更新:2024-07-29

Java(供 Kotlin 使用)

避免使用硬关键字

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

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

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 端并没有这种强制执行。如果没有包装器或防御性拷贝,返回长期持有的集合引用可能会破坏不变量。

伴生对象函数

伴生对象 (companion object) 中的公共函数必须使用 @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 使用)中的问题。具体支持的检查如下

  • 未知可空性 (Unknown Nullness)
  • 属性访问 (Property Access)
  • 无硬 Kotlin 关键字 (No Hard Kotlin keywords)
  • Lambda 参数置后 (Lambda Parameters Last)

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