本文档提供了关于用 Java 和 Kotlin 编写公共 API 的一系列规则,目的是让您从另一种语言使用代码时感觉其符合语言习惯。
Java(供 Kotlin 使用)
不得使用硬关键字
请勿将 Kotlin 的任何硬关键字用作方法或字段的名称。从 Kotlin 调用时,这些硬关键字需要使用反引号进行转义。允许使用软关键字、修饰符关键字和特殊标识符。
例如,从 Kotlin 使用时,Mockito 的 when
函数需要使用反引号:
val callable = Mockito.mock(Callable::class.java) Mockito.`when`(callable.call()).thenReturn(/* … */)
避免使用 Any
的扩展函数或属性的名称
除非绝对必要,否则应避免对方法使用 Any
的扩展函数的名称或对字段使用 Any
的扩展属性的名称。虽然成员方法和字段始终优先于 Any
的扩展函数或属性,但读取代码时可能很难知道调用的是哪个。
可为 null 性注解
公共 API 中的每个非基元参数类型、返回类型和字段类型都应具有可为 null 性注解。不带注解的类型会被解释为“平台”类型,而后者是否可为 null 不明确。
默认情况下,Kotlin 编译器标志接受 JSR 305 注解,但进行标记时会发出警告。您还可以设置一个标志,以使编译器将注解视为错误。
Lambda 参数位于最后
符合 SAM 转换条件的参数类型应位于最后。
例如,RxJava 2’s Flowable.create()
方法签名定义为:
public staticFlowable create( FlowableOnSubscribe 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)
如果要将方法作为属性提供,请勿使用“has”/“set”之类的非标准前缀或不带“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 是一个突出的例子,因为它旨在将 this
与 other
进行比较,而 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 Optionaldata class Some (val value: T): Optional () object None : Optional () @JvmName("ofNullable") fun 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"; OptionaloptionalString = 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 使用)中的问题。具体来说,支持的检查包括:
- 未知 Null 性
- 属性访问
- 不得使用 Kotlin 硬关键字
- Lambda 参数位于最后
Android Studio
要启用这些检查,请依次转到 File > Preferences > Editor > Inspections,然后在“Kotlin Interoperability”下勾选要启用的规则:
图 1. Android Studio 中的 Kotlin 互操作性设置。
勾选要启用的规则后,当您运行代码检查(依次转到 Analyze > Inspect Code…)时,将运行新的检查。
命令行 build
如需通过命令行 build 启用这些检查,请在 build.gradle
文件中添加以下代码行:
Groovy
android { ... lintOptions { enable 'Interoperability' } }
Kotlin
android { ... lintOptions { enable("Interoperability") } }
如需了解 lintOptions 内支持的全部配置,请参阅 Android Gradle DSL 参考文档。
然后,从命令行运行 ./gradlew lint
。