如何在Java代码中更优雅地调用Kotlin

如何在Java代码中更优雅地调用Kotlin_第1张图片
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实现静态访问,并有选择的屏蔽setget方法

@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("")

  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 fieldget/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自动可以用在所有List的地方,但Java不支持型变,需要使用?通配符作为参数:<?extends ...> 或者

@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定义的类型,不过问题不大。

你可能感兴趣的:(Kotlin,java,kotlin,互操作)