在 Java 中调用 Kotlin 代码
在 Java 中可以很容易地调用 Kotlin 代码. 比如, 在 Java 方法中可以非常自然的创建 Kotlin 类的实例, 并操作这些实例. 但是 Java 和 Kotlin 之间还是存在一些差异, 在 Java 中集成 Kotlin 代码时, 需要注意这些问题. 本章中, 我们将会介绍在 Java 代码中如何调用 Kotlin 代码.
Kotlin 的属性会被编译为以下 Java 元素:
一个取值方法, 方法名由属性名加上
get
前缀得到.一个设值方法, 方法名由属性名加上
set
前缀得到 (只会为var
属性生成设值方法).一个私有的域变量, 名称与属性名相同 (只会为拥有后端域变量的属性生成域变量).
比如, var firstName: String
编译后的结果等于以下 Java 声明:
private String firstName;
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
如果属性名以 is
开头, 会使用另一种映射规则: 取值方法名称会与属性名相同, 设值方法名称等于将属性名中的 is
替换为 set
. 比如, 对于 isOpen
属性, 取值方法名称是 isOpen()
, 设值方法名称是 setOpen()
. 这个规则适用于任何数据类型的属性, 而不仅限于 Boolean
类型.
在源代码文件 app.kt
的 org.example
包内声明的所有函数和属性, 包括扩展函数, 都被会编译成为 Java 类 org.example.AppKt
的静态方法.
// 源代码文件: app.kt
package org.example
class Util
fun getTime() { /*...*/ }
// Java 代码
new org.example.Util();
org.example.AppKt.getTime();
要改变编译生成的 Java 类的名称, 可以使用 @JvmName
注解:
@file:JvmName("DemoUtils")
package org.example
class Util
fun getTime() { /*...*/ }
// Java 代码
new org.example.Util();
org.example.DemoUtils.getTime();
如果多个源代码文件生成的 Java 类名相同(由于文件名和包名都相同, 或由于使用了相同的 @JvmName
注解) 这样的情况通常会被认为是错误. 但是, 编译器能够使用指定的名称生成单个 Java Facade 类, 其中包含所有源代码文件的所有内容. 要生成这样的 Facade 类, 可以在多个源代码文件中使用 @JvmMultifileClass
注解.
// 源代码文件: oldutils.kt
@file:JvmName("Utils")
@file:JvmMultifileClass
package org.example
fun getTime() { /*...*/ }
// 源代码文件: newutils.kt
@file:JvmName("Utils")
@file:JvmMultifileClass
package org.example
fun getDate() { /*...*/ }
// Java 代码
org.example.Utils.getTime();
org.example.Utils.getDate();
如果希望将一个 Kotlin 属性公开为 Java 中的一个域, 需要对它添加 @JvmField
注解. 生成的域的可见度与属性可见度一样. 要对属性使用 @JvmField
注解, 属性需要满足以下条件:
拥有后端域变量(backing field)
不是 private 属性
没有
open
,override
或const
修饰符不是委托属性(delegated property)
class User(id: String) {
@JvmField val ID = id
}
// Java 代码
class JavaClient {
public String getID(User user) {
return user.ID;
}
}
延迟初始化属性 也会公开为 Java 中的域. 域的可见度与属性的 lateinit
的设值方法可见度一样.
声明在命名对象(named object)或同伴对象(companion object)之内的 Kotlin 属性, 会存在静态的后端域变量(backing field), 对于命名对象, 静态后端域变量存在于命名对象内, 对于同伴对象, 静态后端域变量存在包含同伴对象的类之内.
通常这些静态的后端域变量是 private 的, 但可以使用以下方法来公开它:
使用
@JvmField
注解使用
lateinit
修饰符使用
const
修饰符
如果对这样的属性添加 @JvmField
注解, 那么它的静态后端域变量可见度将会与属性本身的可见度一样.
class Key(val value: Int) {
companion object {
@JvmField
val COMPARATOR: Comparator<Key> = compareBy<Key> { it.value }
}
}
// Java 代码
Key.COMPARATOR.compare(key1, key2);
// 这里访问的是 Key 类中的 public static final 域
命名对象或同伴对象中的延迟初始化属性 对应的静态的后端域变量, 其可见度将与属性的设值方法可见度一样.
object Singleton {
lateinit var provider: Provider
}
// Java 代码
Singleton.provider = new Provider();
// 这里访问的是 Singleton 类中的 public static 非 final 域
声明为 const
的属性(无论定义在类中, 还是在顶级范围内(top level)) 会被转换为 Java 中的静态域:
// 源代码文件: example.kt
object Obj {
const val CONST = 1
}
class C {
companion object {
const val VERSION = 9
}
}
const val MAX = 239
在 Java 中:
int constant = Obj.CONST;
int max = ExampleKt.MAX;
int version = C.VERSION;
上文中提到, Kotlin 会将包级函数编译为静态方法. 此外, 如果你对函数添加 @JvmStatic
注解, Kotlin 也可以为命名对象或同伴对象中定义的函数生成静态方法. 如果使用这个注解, 编译器既会在对象所属的类中生成静态方法, 同时也会在对象中生成实例方法. 比如:
class C {
companion object {
@JvmStatic fun callStatic() {}
fun callNonStatic() {}
}
}
现在, callStatic()
在 Java 中是一个静态方法, 而 callNonStatic()
不是:
C.callStatic(); // 正确
C.callNonStatic(); // 错误: 不是静态方法
C.Companion.callStatic(); // 实例上的方法仍然存在
C.Companion.callNonStatic(); // 这个方法只能通过实例来调用
与此类似, 对于命名对象:
object Obj {
@JvmStatic fun callStatic() {}
fun callNonStatic() {}
}
在 Java 中:
Obj.callStatic(); // 正确
Obj.callNonStatic(); // 错误
Obj.INSTANCE.callNonStatic(); // 正确, 这是对单体实例的一个方法调用
Obj.INSTANCE.callStatic(); // 也正确
从 Kotlin 1.3 开始, 接口的同伴对象中定义的函数也可以使用 @JvmStatic
注解. 这类函数会被编译为接口中的静态方法. 注意, 从 Java 1.8 才开始支持接口中的静态方法, 因此注意编译时需要选择正确的 JVM 目标平台.
interface ChatBot {
companion object {
@JvmStatic fun greet(username: String) {
println("Hello, $username")
}
}
}
你也可以将 @JvmStatic
注解用于命名对象或同伴对象的属性, 可以使得属性的取值方法和设值方法变成静态方法, 对于命名对象, 这些静态方法在命名对象之内, 对于同伴对象, 这些静态方法在包含同伴对象的类之内.
在使用 JVM 平台时, Kotlin 会将接口中声明的函数编译为 默认方法(Default Method) unless configured otherwise. 这些方法是接口中的实体方法, Java 类可以直接继承得到, 不需要重新实现.
下面是一个有默认方法的 Kotlin 接口的示例:
interface Robot {
fun move() { println("~walking~") } // 在 Java 接口中, 这个方法将成为默认方法
fun speak(): Unit
}
对于实现这个接口的 Java 类来说, 这个方法已经存在一个默认实现.
// Java 实现类
public class C3PO implements Robot {
// Robot 接口中隐含存在 move() 方法的实现
@Override
public void speak() {
System.out.println("I beg your pardon, sir");
}
}
C3PO c3po = new C3PO();
c3po.move(); // 这里会调用 Robot 接口中的默认实现
c3po.speak();
接口的实现类也可以覆盖默认方法.
// Java 代码
public class BB8 implements Robot {
// 通过自己的实现覆盖默认方法
@Override
public void move() {
System.out.println("~rolling~");
}
@Override
public void speak() {
System.out.println("Beep-beep");
}
}
Kotlin 提供了 3 种模式来控制接口中的函数如何编译为 JVM 默认方法. 这些模式决定编译器是否生成兼容性桥接函数(Compatibility Bridge)和 DefaultImpls
类中的静态方法.
你可以使用 -jvm-default
编译选项来控制这个行为:
note
-jvm-default
编译选项替代了已废弃的-Xjvm-default
选项.
兼容模式的详细说明如下:
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
不变.
调用 Kotlin 中的方法时, 有时你可能会需要使用 KClass
类型的参数. Java 的 Class
不会自动转换为 Kotlin 的 KClass
, 因此你必须手动进行转换, 方法是使用 Class<T>.kotlin
扩展属性, 这个扩展属性对应的方法是:
kotlin.jvm.JvmClassMappingKt.getKotlinClass(MainView.class)
有时候我们在 Kotlin 中声明了一个函数, 但在 JVM 字节码中却需要一个不同的名称. 最显著的例子就是 类型消除(type erasure) 时的情况:
fun List<String>.filterValid(): List<String>
fun List<Int>.filterValid(): List<Int>
这两个函数无法同时定义, 因为它们产生的 JVM 代码的签名是完全相同的: filterValid(Ljava/util/List;)Ljava/util/List;
. 如果我们确实需要在 Kotlin 中给这两个函数定义相同的名称, 那么可以对其中一个(或两个)使用 @JvmName
注解, 通过这个注解的参数来指定一个不同的名称:
fun List<String>.filterValid(): List<String>
@JvmName("filterValidInt")
fun List<Int>.filterValid(): List<Int>
在 Kotlin 中, 可以使用相同的名称 filterValid
来访问这两个函数, 但在 Java 中函数名是 filterValid
和 filterValidInt
.
如果我们需要定义一个属性 x
, 同时又定义一个函数 getX()
, 这时也可以使用同样的技巧:
val x: Int
@JvmName("getX_prop")
get() = 15
fun getX() = 10
如果想要改变编译生成的属性访问方法的名称, 又不希望明确地实现属性的取值和设值方法, 你可以使用 @get:JvmName
和 @set:JvmName
:
@get:JvmName("x")
@set:JvmName("changeX")
var x: Int = 23
通常, 如果在 Kotlin 中定义一个函数, 并指定了参数默认值, 这个方法在 Java 中只会存在带所有参数的版本. 如果你希望 Java 端的使用者看到不同参数的多个重载方法, 那么可以使用 @JvmOverloads
注解.
这个注解也可以用于构造器, 静态方法, 等等. 但不能用于抽象方法, 包括定义在接口内的方法.
class Circle @JvmOverloads constructor(centerX: Int, centerY: Int, radius: Double = 1.0) {
@JvmOverloads fun draw(label: String, lineWidth: Int = 1, color: String = "red") { /*...*/ }
}
对于每个带有默认值的参数, 都会生成一个新的重载方法, 这个重载方法的签名将会删除这个参数, 以及右侧的所有参数. 上面的示例程序生成的结果如下:
// 构造器:
Circle(int centerX, int centerY, double radius)
Circle(int centerX, int centerY)
// 方法
void draw(String label, int lineWidth, String color) { }
void draw(String label, int lineWidth) { }
void draw(String label) { }
注意, 在 次级构造器 中介绍过, 如果一个类的构造器方法参数全部都指定了默认值, 那么会对这个类生成一个 public 的无参数构造器. 这个特性即使在没有使用 @JvmOverloads
注解时也是有效的.
Kotlin 中不存在受控异常. 因此, Kotlin 函数在 Java 中的签名通常不会声明它抛出的异常. 因此, 假如你有一个这样的 Kotlin 函数:
// 源代码文件: example.kt
package demo
fun writeToFile() {
/*...*/
throw IOException()
}
然后你希望在 Java 中调用它, 并捕获异常:
// Java 代码
try {
demo.Example.writeToFile();
} catch (IOException e) {
// 错误: writeToFile() 没有声明抛出 IOException 异常
// ...
}
这时 Java 编译器会报告错误, 因为 writeToFile()
没有声明抛出 IOException
异常. 为了解决这个问题, 可以在 Kotlin 中使用 @Throws
注解:
@Throws(IOException::class)
fun writeToFile() {
/*...*/
throw IOException()
}
在 Java 中调用 Kotlin 函数时, 没有任何机制阻止我们向一个非 null 参数传递一个 null
值. 所以, Kotlin 编译时, 会对所有接受非 null 值参数的 public 方法产生一些运行时刻检查代码. 由于这些检查代码的存在, Java 端代码会立刻得到一个 NullPointerException
异常.
如果 Kotlin 类使用了 声明处的类型变异(declaration-site variance), 那么这些类在 Java 代码中看到的形式存在两种可能. 比如, 你有下面这样的类, 以及两个使用这个类的函数:
class Box<out T>(val value: T)
interface Base
class Derived : Base
fun boxDerived(value: Derived): Box<Derived> = Box(value)
fun unboxBase(box: Box<Base>): Base = box.value
如果用最简单的方式转换为 Java 代码, 结果将是:
Box<Derived> boxDerived(Derived value) { ... }
Base unboxBase(Box<Base> box) { ... }
问题在于, 在 Kotlin 中你可以这样: unboxBase(boxDerived(Derived()))
, 但在 Java 中却不可以, 因为在 Java 中, Box
的类型参数 T
是 不可变的(invariant), 因此 Box<Derived>
不是 Box<Base>
的子类型. 为了解决 Java 端的问题, 你需要将 unboxBase
函数定义成这样:
Base unboxBase(Box<? extends Base> box) { ... }
这个声明使用了 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 编程风格的). 因此, 我们上面例子中的函数真正的输出结果是这样的:
// 返回值 - 没有类型通配符
Box<Derived> boxDerived(Derived value) { ... }
// 参数 - 有类型通配符
Base unboxBase(Box<? extends Base> box) { ... }
note
如果类型参数是 final 的, 那么生成类型通配符一般来说就没有意义了, 因此
Box<String>
永远是Box<String>
, 无论它出现在什么位置.
如果你需要类型通配符, 但默认没有生成, 可以使用 @JvmWildcard
注解:
fun boxDerived(value: Derived): Box<@JvmWildcard Derived> = Box(value)
// 将被翻译为
// Box<? extends Derived> boxDerived(Derived value) { ... }
反过来, 如果默认生成了类型通配符, 但你不需要它, 可以使用 @JvmSuppressWildcards
注解:
fun unboxBase(box: Box<@JvmSuppressWildcards Base>): Base = box.value
// 将被翻译为
// Base unboxBase(Box<Base> box) { ... }
note
@JvmSuppressWildcards
不仅可以用于单个的类型参数, 也可以用于整个函数声明或类声明, 这时它会使得这个函数或类之内的所有类型通配符都不产生.
Nothing
类型是很特殊的, 因为它在 Java 中没有对应的概念. 所有的 Java 引用类型, 包括java.lang.Void
, 都可以接受 null
作为它的值, 而 Nothing
甚至连 null
值都不能接受. 因此, 在 Java 的世界里无法准确地表达这个类型. 因此, Kotlin 会在使用 Nothing
类型参数的地方生成一个原生类型(raw type):
fun emptyList(): List<Nothing> = listOf()
// 将被翻译为
// List emptyList() { ... }
如果希望 Java 代码能够与 Kotlin 的 内联的值类(Inline Value Class) 顺利协作, 可以使用 @JvmExposeBoxed
注解, 或 -Xjvm-expose-boxed
编译器选项. 这些方法可以确保 Kotlin 为与 Java 的互操作生成必要的装箱表达形式(Boxed Representation).
默认情况下, Kotlin 会将内联的值类编译为使用 未装箱的表达形式(Unboxed Representation), 在 Java 中通常无法使用. 例如, 无法在 Java 中调用 MyInt
类的构造器:
@JvmInline
value class MyInt(val value: Int)
因此下面的 Java 代码会失败:
MyInt input = new MyInt(5);
可以使用 @JvmExposeBoxed
注解, 让 Kotlin 生成可在 Java 中直接调用的 public 构造器. 可以将注解应用于以下层级, 确保对 Java 公开的内容进行粒度控制:
类
构造器
函数
在你的代码中使用 @JvmExposeBoxed
注解之前, 必须使用 @OptIn(ExperimentalStdlibApi::class)
, 以表示使用者同意(Opt-in). 例如:
@OptIn(ExperimentalStdlibApi::class)
@JvmExposeBoxed
@JvmInline
value class MyInt(val value: Int)
@OptIn(ExperimentalStdlibApi::class)
@JvmExposeBoxed
fun MyInt.timesTwoBoxed(): MyInt = MyInt(this.value * 2)
有了这些注解, Kotlin 会为 MyInt
类生成 Java 能够访问的构造器, 并且 为扩展函数生成一个变体, 它使用值类的装箱形式. 因此下面的 Java 代码能够成功运行:
MyInt input = new MyInt(5);
MyInt output = ExampleKt.timesTwoBoxed(input);
要将这个行为应用于模块中所有的内联值类, 以及所有使用它们的函数, 请使用 -Xjvm-expose-boxed
选项编译这个模块. 使用这个选项进行编译, 效果等于模块中所有的声明都带有 @JvmExposeBoxed
注解.