在Java代码中更优雅地调用Kotlin
-
Kotlin与Java良好的互操作性是其能够快速普及的原因之一。从Java虽然可以访问Kotlin,但是通过下面这些技巧可以让对Kotlin的访问变得更加友好和地道
@JvmStatic
Kotlin中可以使用object class
创建单例
object Analytics {
fun init() {...}
fun send(event: Event) {...}
fun close() {...}
}
Kotlin侧可以像Java的静态方法一样访问其方法
Analytics.send(Event("custom_event"))
但Java侧会生成INSTANCE
单例对象,使用起来有些啰嗦
Analytics.INSTANCE.send(new Event("custom_event"));
如何在Java中也能像访问静态方法一样访问object class?
使用@JvmStatic
注解
object Analytics {
@JvmStatic fun init() {...}
@JvmStatic fun send(event: Event) {...}
@JvmStatic fun close() {...}
}
...
// Java call-site
Analytics.send(new Event("custom_event"));
除了方法以外,我们可以对public的属性添加@JvmStatic实现静态访问,并有选择的屏蔽set
或get
方法
@JvmStatic var isInited: Boolean = false
private set
-
@JvmOverloads
data class Event(val name: String, val context: Map<String, Any> =
emptyMap())
kotlin的默认参数
让方法拥有更多签名,实现方法重载的效果,可以使用两种构造函数构造Event:
Event("")
Event("", map)
Java不支持默认参数,但是使用@JvmOverloads
,可以在Java侧生成多个重载方法:
data class Event @JvmOverloads constructor(val name: String, val context: Map<String, Any> = emptyMap())
-
@Throws
Kotlin中没有Checked Exception
interface Plugin {
fun init()
/** @throws IOException if sending failed */
fun send(event: Event)
fun close()
}
如上,send
方法虽然有可能抛出IOException
,但是编译器不强制需要try...catch...
// object Analytics
@JvmStatic fun send(event: Event) {
log("<Internal Send event>")
plugins.forEach {
try {
it.send(event)
} catch (e: IOException) {
log("WARN: ${it.javaClass.simpleName} fired IOE")
}
}
}
由于send在运行时有可能抛出IOException,Java中实现Plugin
接口时,为了安全性考虑可能会为send添加throws
声明,此时编译器会给出错误
Overridden method does not throw IOException
使用@Throw
可以在Java中让编译器知道这个方法会抛出CE
interface Plugin {
fun init()
/** @throws IOException if sending failed */
@Throws(IOException::class)
fun send(event: Event)
fun close()
}
-
@JvmName
属性
Kotlin的属性在Java侧会翻译成配套的get/set方法,而且遵循Java的命名习惯,使用起来非常友好:
- name : String -> getName()/setName()
- isName: boolean -> isName()/setName()
但是像hasName
这样的属性,就没那么智能了,会翻译成getHasName()
。
如何指定成hasName()
呢?使用@JvmName
val hasName @JvmName("hasName") get() = mNames.isNotEmpty()
也可以针对get/set选择性的指定
@get:JvmName("hasName") val hasName get() =
mNames.isNotEmpty()
方法
如果我们定义如下两个扩展方法:
fun List<Int>.printReversedSum() {
println(this.foldRight(0) { it, acc -> it + acc })
}
fun List<String>.printReversedSum() {
println(this.foldRight(StringBuilder()) {
it, acc -> acc.append(it)
})
}
由于泛型擦除,经过字节码反编译Java后的签名无法区分:
public static final void printReversedSum(@NotNull List $receiver)
此时可以借助于@JvmName
fun List<Int>.printReversedSum() {
println(this.foldRight(0) { it, acc -> it + acc })
}
@JvmName("printReversedConcatenation")
fun List<String>.printReversedSum() {
println(this.foldRight(StringBuilder()) {
it, acc -> acc.append(it)
})
}
文件
Kotlin支持top-level的方法或属性,但是Java的方法和属性只能存在与Class中,所以Java侧会为顶级方法/属性作为静态成员定义在一个文件名+kt
后缀的Class中。
使用@JvmName可以对这个Class的名字进行指定:
//reverser.kt
package util
fun String.reverse() = StringBuilder(this).reverse().toString()
如上,Java中使用ReverserKt
访问静态方法
//reverser.kt
@file:JvmName("ReverserUtils")
package util
fun String.reverse() = StringBuilder(this).reverse().toString()
如上,Java中使用ReverserUtils
访问静态方法,更加友好
@JvmMultifileClass
当对多个文件使用@JvmName并指定同一个名字时,编译器会提示重复定义,此时可以@JvmMultifileClass
将Kt侧多个文件在Java中合成一个,避免编译器出错:
//Collections.kt
@file:kotlin.jvm.JvmMultifileClass
@file:kotlin.jvm.JvmName("CollectionsKt")
package kotlin.collections
-----
//Iterables.kt
@file:kotlin.jvm.JvmMultifileClass
@file:kotlin.jvm.JvmName("CollectionsKt")
package kotlin.collections
-
@JvmField
@JvmField
可以消除backing field
的get
/set
,让其在java侧向var
一样被访问:
class KotlinJvmSample {
@JvmField
val example = "Hello!"
}
companion object
中的公共函数必须用使用 @JvmStatic 注解才能暴露为静态方法。-
如果没有这个注解,这些函数仅可用作静态 Companion
字段上的实例方法。
class Sample {
companion object {
@JvmField val MAX_LIMIT = 20
}
}
相当于
public class Sample {
public static final int MAX_LIMIT = 20;
}
-
@JvmWildcard
这个注解主要用于处理泛型参数,这涉及**型变(逆变与协变)**的知识点。-
Kotlin支持型变,List<Dog>
自动可以用在所有List<Animal>
的地方,但Java不支持型变,需要使用?通配符作为参数:<?extends ...>
或者 <? super …>
@JvmWildcard
用来帮助在Java侧生成通配符。
在返回值出现泛型类型时:
fun getPlugins(): List<Plugin>
在Java侧无法生成通配符
public final List<Plugin> getPlugins()
添加@JvmWildcard
后
fun getPlugins(): List<@JvmWildcard Plugin>
可以生成通配符
public final List<? extends Plugin> getPlugins()
@JvmSuppressWildcards
在入参有泛型类型时
fun addPlugins(plugs: List<Plugin>)
上面代码在Java中会自动添加通配符
public final void addPlugins(@NotNull List<? extends Plugin> plugs)
@JvmSuppressWildcards
与@JvmWildcard
相反,是抑制通配符的生成。
添加的位置不同作用范围不同:
// Suppress only for this param.
fun addPlugins(plugs: List<@JvmSuppressWildcards Plugin>)
// Suppress for the whole method.
@JvmSuppressWildcards fun addPlugins(plugs: List<Plugin>)
// Or even for the whole class.
@JvmSuppressWildcards
object Analytics {
fun addPlugins(plugs: List<Plugin>)
}
-
@JvmSynthetic
有时候有些Kotlin方法不想暴露给Java,此时可以使用@JvmSynthetic
,此外还有一个trick的方法就是使用@JvmName("")定义一个Java中非法的名字,这样也可以隐藏Kotlin的方法。
@ JvmDefault
Kotlin1.3新增的操作符,可以让Kotlin的interface中的default方法出现在Java中,-
详细可参考:Java与Kotlin混合编程之@JvmDefault
Java访问Kotlin的其他注意事项
Builder
Kotlin中我们有apply
等作用域函数,一般不需要使用Builder,但是为了兼容Java,我们可以定义Builder如下:
class Builder {
private lateinit var baseUrl: String
private var client: Client = Client()
fun baseUrl(baseUrl: String) = this.also { it.baseUrl = baseUrl }
fun client(client: Client) = this.also { it.client = client }
fun build() = Retrofit(baseUrl, client)
}
Kotlin写builder也如此方便:对于必需的字段使用lateinit
,通过also{ }
还可以避免return this
Internal
Kotlin的Internal在Java中仍然是可见的,需要特别注意,如果不想暴露可以添加@JvmSynthetic
Null-checks
Kotlin的方法参数或者返回值,在Java中会根据其可空性添加@Nonnull
或 @Nullable
,有助于IDE的提醒。
Inline函数
Inline方法在Java中可以正常访问,但是有reified
参数的inline函数无法访问,因为Java无法处理reified
Unit
Kotlin侧的Lambda返回Unit
时,Java必须强制处理一下return
,如下
ReverserUtils.forEachReversed(list, it -> {
System.out.println(it);
return null;
});
Typealiases
Java无法访问Typealiases
定义的类型,不过问题不大。