本文档是一套用于在 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();
}
}
伴随对象常量
在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 互操作性下选中要启用的规则
选中要启用的规则后,在你运行代码检查(**分析 > 检查代码…**)时,新的检查将运行。
命令行构建
要从命令行构建中启用这些检查,请在你的build.gradle
文件中添加以下行
Groovy
android { ... lintOptions { enable 'Interoperability' } }
Kotlin
android { ... lintOptions { enable("Interoperability") } }
有关在 lintOptions 内部支持的完整配置集,请参阅 Android 的Gradle DSL 参考。
然后,从命令行运行./gradlew lint
。