本文翻译自 Android Kotlin Guides 的 Interop Guide,翻译项目地址为:https://github.com/msdx/kotlin-guides-cn ,欢迎关注及校正。
这是一组关于使用 Java 和 Kotlin 语言编写公共 API 的规则,目的是让代码在其他语言使用时也会感到习惯。
更新于:2018-05-18
不要使用 Kotlin 的硬性关键字作为方法或字段的名称,因此它们会让 Kotlin 在调用时需要使用反引号来避免与其冲突。软关键字,修饰符关键字和特殊标识符则允许使用。
例如,Mockito 的 when
函数在 Kotlin 使用时就需要反引号:
val callable = Mockito.mock(Callable::class.java)
Mockito.`when`(callable.call()).thenReturn(/* … */)
公共 API 中的每个非原始类型的参数、返回值和字段的类型都应该有一个可为空性的注解。没有这种注解的类型会被当作不确定是否可为空的“平台”类型。
JSR 305包注解可用于设置合理的默认值,但目前不建议使用。它们需要一个选择加载的标志位才能被编译器认可,并且还与 Java 9 的模块系统冲突。
符合 SAM 转换的参数类型应该放到最后。
例如,RxJava 2 的 Flowable.create()
方法签名定义为:
public static Flowable create(
FlowableOnSubscribe source,
BackpressureStrategy mode) { /* … */ }
因为 FlowableOnSubscribe
适合进行 SAM 转换,所以 Kotlin 对此方法的函数调用如下所示:
Flowable.create({ /* … */ }, BackpressureStrategy.LATEST)
但是,如果方法签名中的参数调换过来,则函数调用可以使用 trailing-lambda 语法:
Flowable.create(BackpressureStrategy.LATEST) { /* … */ }
对于要在 Kotlin 中表示为属性的方法,必须使用严格的 “bean” 风格的前缀。
访问器方法需要以 “get” 为前缀,如果是 boolean
类型的返回方法,使用 “is” 前缀。
public final class User {
public String getName() { /* … */ }
public boolean isActive() { /* … */ }
}
val name = user.name // 会调用 user.getName()
val active = user.active // 会调用 user.isActive()
对应的改值方法则需要 “set” 前缀。
public final class User {
public String getName() { /* … */ }
public void setName(String name) { /* … */ }
}
user.name = "Bob" // 会调用 user.setName(String)
如果您希望将方法公开为属性,请不要使用非标准前缀,如 “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)
当一个文件包含顶级函数或属性时,始终用 @file:JvmName("Foo)"
注解以提供一个好的名字。
默认情况下,文件 “Foo.kt” 中的顶级成员最终会在一个名为 “FooKt” 的类中,这个类很没有吸引力并且泄漏了语言作为实现的细节。
考虑添加 @file:JvmMultifileClass
注解将多个文件中的顶级成员合并为一个类。
要在 Java 中使用的函数类型应避免使用 Unit
返回类型。那样做的话就需要指定一条明确的 return Unit.INSTANCE;
语句,而这并不符合我们的语言习惯。
fun sayHi(callback: (String) -> Unit) = /* … */
// Kotlin caller:
greeter.sayHi { Log.d("Greeting", "Hello, $it!") }
// Java caller:
greeter.sayHi(name -> {
Log.d("Greeting", "Hello, " + name + "!");
return Unit.INSTANCE;
});
这一语法也不允许提供语义命名的类型,以便能在其他类型上实现。
在 Kotlin 中为 lambda 类型定义一个命名的单抽象方法(SAM)接口,可以纠正 Java 的问题,但是也阻止了在 Kotlin 中使用 lambda 语法。
interface GreeterCallback {
fun greetName(name: String): Unit
}
fun sayHi(callback: GreeterCallback) = /* … */
// Kotlin 调用者
greeter.sayHi(object : GreeterCallback {
override fun greetName(name: String) {
Log.d("Greeting", "Hello, $name!")
}
})
// Java 调用者
greeter.sayHi(name -> Log.d("Greeting", "Hello, " + name + "!"));
在 Java 中定义一个命名的 SAM 接口,允许 Kotlin 使用较低级的 lambda 语法版本,其中必须明确指定接口类型。
// 在 Java 中定义
interface GreeterCallback {
void greetName(String name);
}
fun sayHi(greeter: GreeterCallback) = /* … */
// Kotlin 调用者:
greeter.sayHi(GreeterCallback { Log.d("Greeting", "Hello, $it!") })
// Java 调用者:
greeter.sayHi(name -> Log.d("Greeter", "Hello, " + name + "!"));
目前还没有办法为在 Java 和 Kotlin 使用的 lambda 定义一种参数类型,使得它符合这两种语言的使用习惯。目前的建议是更推荐使用函数类型,尽管当返回类型为 Unit
时,在 Java 上体验不佳。
注意:此建议将来可能会有变化。见 KT-7770 和 KT-21018。
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
字段中奇怪命名的 ‘getters’ 实例。而只使用 @JvmStatic
而不是 @JvmField
的话,会将奇怪命名的 ‘getters’ 移到类的静态方法中,但仍然是不正确的。
不正确:没有注解
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)
// 从 KOTLIN 中调用:
fun main(vararg args: String) {
val nullableString: String? = "foo"
val optionalString = nullableString.asOptional()
}
// 从 JAVA 中调用:
public static void main(String... args) {
String nullableString = "Foo";
Optional 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");
}
}
现在有 Android Lint 检查可以帮助你检测和标记上述一些互通性的问题。目前仅检测 Java 中的问题(被 Kotlin 调用)。具体来说,所支持的检查有:
要启用这些检查,请转到 File > Preferences> Editor > Inspections,并检查你想要在 Kotlin 互通性下启用的规则:
检查了要启用的规则后,将在运行代码检查(Analyze > Inspect Code…)时运行新的检查
要从命令行构建中启用这些检查,请在 build.gradle
文件中添加以下行:
android {
...
lintOptions {
check 'Interoperability'
}
}
有关 lintOptions 内支持的完整配置,请参阅 Android Gradle DSL 参考。
然后,在命令行中运行./gradlew lint
。