本文档是一套在 Java 和 Kotlin 中编写公共 API 的规则,旨在使代码在从另一种语言使用时感觉更符合习惯用法。
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)
如果您希望方法以属性的形式公开,请勿使用非标准前缀,例如 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) 可以在 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 是一个突出的例子,因为它旨在将 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();
}
}
伴生常量
伴生对象中作为有效常量的公共非 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
。