在 Java 中调用 Kotlin 代码
在 Java 中可以很容易地调用 Kotlin 代码. 比如, 在 Java 方法中可以非常自然的创建 Kotlin 类的实例, 并操作这些实例. 但是 Java 和 Kotlin 之间还是存在一些差异, 在 Java 中集成 Kotlin 代码时, 需要注意这些问题. 本章中, 我们将会介绍在 Java 代码中如何调用 Kotlin 代码.
属性
Kotlin 的属性会被编译为以下 Java 元素:
一个取值方法, 方法名由属性名加上
get
前缀得到一个设值方法, 方法名由属性名加上
set
前缀得到 (只会为var
属性生成设值方法)一个私有的域变量, 名称与属性名相同 (只会为拥有后端域变量的属性生成域变量)
比如, var firstName: String
编译后的结果等于以下 Java 声明:
如果属性名以 is
开头, 会使用另一种映射规则: 取值方法名称会与属性名相同, 设值方法名称等于将属性名中的 is
替换为 set
. 比如, 对于 isOpen
属性, 取值方法名称将会是 isOpen()
, 设值方法名称将会是 setOpen()
. 这个规则适用于任何数据类型的属性, 而不仅限于 Boolean
类型.
包级函数
在源代码文件 app.kt
的 org.example
包内声明的所有函数和属性, 包括扩展函数, 都被会编译成为 Java 类 org.example.AppKt
的静态方法.
要改变编译生成的 Java 类的名称, 可以使用 @JvmName
注解:
如果多个源代码文件生成的 Java 类名相同(由于文件名和包名都相同, 或由于使用了相同的 @JvmName
注解) 这样的情况通常会被认为是错误. 但是, 编译器能够使用指定的名称生成单个 Java Facade 类, 其中包含所有源代码文件的所有内容. 要生成这样的 Facade 类, 可以在多个源代码文件中使用 @JvmMultifileClass
注解.
实例的域
如果希望将一个 Kotlin 属性公开为 Java 中的一个域, 需要对它添加 @JvmField
注解. 生成的域的可见度将与属性可见度一样. 要对属性使用 @JvmField
注解, 属性需要满足以下条件:
拥有后端域变量(backing field)
不是 private 属性
没有
open
,override
或const
修饰符不是委托属性(delegated property)
延迟初始化属性 也会公开为 Java 中的域. 域的可见度将与属性的 lateinit
的设值方法可见度一样.
静态域(Static Fields)
声明在命名对象(named object)或同伴对象(companion object)之内的 Kotlin 属性, 将会存在静态的后端域变量(backing field), 对于命名对象, 静态后端域变量存在于命名对象内, 对于同伴对象, 静态后端域变量存在包含同伴对象的类之内.
通常这些静态的后端域变量是 private 的, 但可以使用以下方法来公开它:
使用
@JvmField
注解使用
lateinit
修饰符使用
const
修饰符
如果对这样的属性添加 @JvmField
注解, 那么它的静态后端域变量可见度将会与属性本身的可见度一样.
命名对象或同伴对象中的延迟初始化属性 对应的静态的后端域变量, 其可见度将与属性的设值方法可见度一样.
声明为 const
的属性(无论定义在类中, 还是在顶级范围内(top level)) 会被转换为 Java 中的静态域:
在 Java 中:
静态方法(Static Method)
上文中提到, Kotlin 会将包级函数编译为静态方法. 此外, 如果你对函数添加 @JvmStatic
注解, Kotlin 也可以为命名对象或同伴对象中定义的函数生成静态方法. 如果使用这个注解, 编译器既会在对象所属的类中生成静态方法, 同时也会在对象中生成实例方法. 比如:
现在, callStatic()
在 Java 中是一个静态方法, 而 callNonStatic()
不是:
对命名对象也一样:
在 Java 中:
从 Kotlin 1.3 开始, 接口的同伴对象中定义的函数也可以使用 @JvmStatic
注解. 这类函数会被编译为接口中的静态方法. 注意, 从 Java 1.8 才开始支持接口中的静态方法, 因此注意编译时需要选择正确的 JVM 目标平台.
@JvmStatic
注解也可以用于命名对象或同伴对象的属性, 可以使得属性的取值方法和设值方法变成静态方法, 对于命名对象, 这些静态方法在命名对象之内, 对于同伴对象, 这些静态方法在包含同伴对象的类之内.
接口中的默认方法(Default Method)
从 JDK 1.8 开始, Java 中的接口可以包含 默认方法(Default Method). 如果要把 Kotlin 接口的所有非抽象成员变为实现这些接口的 Java 类的默认方法, 请使用编译器选项 -Xjvm-default=all
来编译 Kotlin 代码.
下面是一个有默认方法的 Kotlin 接口的示例:
对于实现这个接口的 Java 类来说, 这个方法已经存在一个默认实现.
接口的实现类也可以覆盖默认方法.
默认方法的兼容模式
如果你的 Kotlin 接口在过去编译时没有使用编译选项 -Xjvm-default=all
, 并且有客户代码正在使用这些接口, 那么, 在你的 Kotlin 接口代码使用这个编译选项再次编译之后, 可能导致客户代码与新代码二进制不兼容. 为了避免对这种客户代码的兼容性造成破坏, 请使用 -Xjvm-default=all
模式, 并使用 @JvmDefaultWithCompatibility
注解标记接口. 这样可以让你一次性的向公开 API 中的所有接口添加这个注解, 而且你不需要对新的非公开代码使用任何注解.
兼容模式的详细解释:
disable 模式
这是默认的模式. 不会生成 JVM 默认方法, 并禁止使用 @JvmDefault
注解.
all 模式
对模块中所有带有函数体的接口声明生成 JVM 默认方法. 不会对带有函数体的接口声明生成 DefaultImpls
桩代码(stub), 在 disable
模式下则默认会生成.
如果接口从一个 disable
模式下编译的接口继承了带有函数体的方法, 并且没有覆盖这个(override)方法, 那么会对这个方法生成 DefaultImpls
桩代码(stub).
如果某些客户代码依赖于 DefaultImpls
类的存在, 那么 会破坏二进制兼容性.
all-compatibility 模式
与 all
模式相比, 还会在 DefaultImpls
类中生成兼容性桩代码(stub). 对于库和运行时的作者来说, 兼容性桩代码可以很有用, 对于那些使用以前的库版本编译的既有的客户代码, 它可以帮助保持向后的二进制兼容性. all
和 all-compatibility
模式改变了库重新编译之后客户代码将要使用的 ABI 界面. 因此, 客户代码可能会与以前的库版本不能兼容. 这通常代表你需要适当的库版本号, 比如, 在语义化版本(SemVer)中增加主版本号.
编译器使用 @Deprecated
注解来生成 DefaultImpls
的所有成员: 你不应该在 Java 代码中使用这些成员, 因为编译器生成这些它们只是为了保持兼容性的目的.
对于从 all
或 all-compatibility
模式下编译的 Kotlin 接口继承的情况, DefaultImpls
兼容性桩代码会使用标准的 JVM 运行期解析语义来调用接口的默认方法.
对继承泛型接口的类, 有些情况下, 在 disable
模式中会生成带有特殊签名的额外的隐含方法, all-compatibility
模式对这些类会执行额外的兼容性检查: 与 disable
模式不同, 如果你没有明确的覆盖这样的方法, 也没有使用 @JvmDefaultWithoutCompatibility
注解标注这个类, 那么编译器会报告错误(详情请参见 这个 YouTrack issue).
可见度
Kotlin 中的可见度修饰符与 Java 的对应规则如下:
private
成员会被编译为 Java 中的private
成员private
顶级声明会被编译为 Java 中的private
顶级声明. 如果从类的内部访问, 那么还会包含包的private
访问器(Package-private accessor).protected
成员在 Java 中仍然是protected
不变 (注意, Java 允许从同一个包内的其他类访问 protected 成员, 但 Kotlin 不允许, 因此 Java 类中将拥有更大的访问权限)internal
声明会被编译为 Java 中的public
.internal
类的成员名称会被混淆, 以降低在 Java 代码中意外访问到这些成员的可能性, 并以此来实现那些根据 Kotlin 的规则相互不可见, 但是其签名完全相同的函数重载public
在 Java 中仍然是public
不变
KClass
调用 Kotlin 中的方法时, 有时你可能会需要使用 KClass
类型的参数. Java 的 Class
不会自动转换为 Kotlin 的 KClass
, 因此你必须手动进行转换, 方法是使用 Class<T>.kotlin
扩展属性, 这个扩展属性对应的方法是:
使用 @JvmName 注解处理签名冲突
有时候我们在 Kotlin 中声明了一个函数, 但在 JVM 字节码中却需要一个不同的名称. 最显著的例子就是 类型消除(type erasure) 时的情况:
这两个函数无法同时定义, 因为它们产生的 JVM 代码的签名是完全相同的: filterValid(Ljava/util/List;)Ljava/util/List;
. 如果我们确实需要在 Kotlin 中给这两个函数定义相同的名称, 那么可以对其中一个(或两个)使用 @JvmName
注解, 通过这个注解的参数来指定一个不同的名称:
在 Kotlin 中, 可以使用相同的名称 filterValid
来访问这两个函数, 但在 Java 中函数名将是 filterValid
和 filterValidInt
.
如果我们需要定义一个属性 x
, 同时又定义一个函数 getX()
, 这时也可以使用同样的技巧:
如果想要改变编译生成的属性访问方法的名称, 又不希望明确地实现属性的取值和设值方法, 你可以使用 @get:JvmName
和 @set:JvmName
:
重载函数(Overload)的生成
通常, 如果在 Kotlin 中定义一个函数, 并指定了参数默认值, 这个方法在 Java 中只会存在带所有参数的版本. 如果你希望 Java 端的使用者看到不同参数的多个重载方法, 那么可以使用 @JvmOverloads
注解.
这个注解也可以用于构造器, 静态方法, 等等. 但不能用于抽象方法, 包括定义在接口内的方法.
对于每个带有默认值的参数, 都会生成一个新的重载方法, 这个重载方法的签名将会删除这个参数, 以及右侧的所有参数. 上面的示例程序生成的结果如下:
注意, 在 次级构造器 中介绍过, 如果一个类的构造器方法参数全部都指定了默认值, 那么会对这个类生成一个 public 的无参数构造器. 这个特性即使在没有使用 @JvmOverloads
注解时也是有效的.
受控异常(Checked Exception)
Kotlin 中不存在受控异常. 因此, Kotlin 函数在 Java 中的签名通常不会声明它抛出的异常. 因此, 假如你有一个这样的 Kotlin 函数:
然后你希望在 Java 中调用它, 并捕获异常:
这时 Java 编译器会报告错误, 因为 writeToFile()
没有声明抛出 IOException
异常. 为了解决这个问题, 可以在 Kotlin 中使用 @Throws
注解:
Null值安全性
在 Java 中调用 Kotlin 函数时, 没有任何机制阻止我们向一个非 null 参数传递一个 null
值. 所以, Kotlin 编译时, 会对所有接受非 null 值参数的 public 方法产生一些运行时刻检查代码. 由于这些检查代码的存在, Java 端代码会立刻得到一个 NullPointerException
异常.
泛型的类型变异(Variant)
如果 Kotlin 类使用了 声明处的类型变异(declaration-site variance), 那么这些类在 Java 代码中看到的形式存在两种可能. 比如, 你有下面这样的类, 以及两个使用这个类的函数:
如果用最简单的方式转换为 Java 代码, 结果将是:
问题在于, 在 Kotlin 中你可以这样: unboxBase(boxDerived(Derived()))
, 但在 Java 中却不可以, 因为在 Java 中, Box
的类型参数 T
是 不可变的(invariant), 因此 Box<Derived>
不是 Box<Base>
的子类型. 为了解决 Java 端的问题, 你需要将 unboxBase
函数定义成这样:
这个声明使用了 Java 的 通配符类型(wildcards type) (? extends Base
), 通过使用处类型变异(use-site variance)来模仿声明处的类型变异(declaration-site variance), 因为 Java 中只有使用处类型变异.
为了让 Kotlin 的 API 可以在 Java 中正常使用, 如果一个类被用作函数参数, 那么对于定义了类型参数协变的 Box
类, 编译器会将 Box<Super>
生成为 Java 的 Box<? extends Super>
(对于定义了类型参数反向协变的 Foo
类, 会生成为 Java 的 Foo<? super Bar>
). 当类被用作返回值时, 编译器不会生成类型通配符, 否则 Java 端的使用者就不得不处理这些类型通配符(而且这是违反通常的 Java 编程风格的). 因此, 我们上面例子中的函数真正的输出结果是这样的:
如果你需要类型通配符, 但默认没有生成, 可以使用 @JvmWildcard
注解:
反过来, 如果默认生成了类型通配符, 但你不需要它, 可以使用 @JvmSuppressWildcards
注解:
Nothing
类型的翻译
Nothing
类型是很特殊的, 因为它在 Java 中没有对应的概念. 所有的 Java 引用类型, 包括java.lang.Void
, 都可以接受 null
作为它的值, 而 Nothing
甚至连 null
值都不能接受. 因此, 在 Java 的世界里无法准确地表达这个类型. 因此, Kotlin 会在使用 Nothing
类型参数的地方生成一个原生类型(raw type):