在Java中調用Kotlin代碼

Java 可以輕鬆調用 Kotlin 代碼。

屬性

Kotlin 屬性會編譯成以下 Java 元素:

  • 一個 getter 方法,名稱通過加前綴 get 算出;
  • 一個 setter 方法,名稱通過加前綴 set 算出(只適用於 var 屬性);
  • 一個私有字段,與屬性名稱相同(僅適用於具有幕後字段的屬性)。

例如,var firstName: String 編譯成以下 Java 聲明:

private String firstName;

public String getFirstName() {
    return firstName;
}

public void setFirstName(String firstName) {
    this.firstName = firstName;
}

如果屬性的名稱以 is 開頭,則使用不同的名稱映射規則:getter 的名稱
與屬性名稱相同,並且 setter 的名稱是通過將 is 替換爲 set 獲得。
例如,對於屬性 isOpen,其 getter 會稱做 isOpen(),而其 setter 會稱做 setOpen()
這一規則適用於任何類型的屬性,並不僅限於 Boolean

包級函數

org.foo.bar 包內的 example.kt 文件中聲明的所有的函數和屬性,包括擴展函數,
都編譯成一個名爲 org.foo.bar.ExampleKt 的 Java 類的靜態方法。

// example.kt
package demo

class Foo

fun bar() {
}
// Java
new demo.Foo();
demo.ExampleKt.bar();

可以使用 [@JvmName](https://github.com/JvmName "@JvmName") 註解修改生成的 Java 類的類名:

@file:JvmName("DemoUtils")

package demo

class Foo

fun bar() {
}
// Java
new demo.Foo();
demo.DemoUtils.bar();

如果多個文件中生成了相同的 Java 類名(包名相同並且類名相同或者有相同的
[@JvmName](https://github.com/JvmName "@JvmName") 註解)通常是錯誤的。然而,編譯器能夠生成一個單一的 Java 外觀
類,它具有指定的名稱且包含來自所有文件中具有該名稱的所有聲明。
要啓用生成這樣的外觀,請在所有相關文件中使用 @JvmMultifileClass 註解。

// oldutils.kt
@file:JvmName("Utils")
@file:JvmMultifileClass

package demo

fun foo() {
}
// newutils.kt
@file:JvmName("Utils")
@file:JvmMultifileClass

package demo

fun bar() {
}
// Java
demo.Utils.foo();
demo.Utils.bar();

實例字段

如果需要在 Java 中將 Kotlin 屬性作爲字段暴露,那就需要使用 [@JvmField](https://github.com/JvmField "@JvmField") 註解對其標註。
該字段將具有與底層屬性相同的可見性。如果一個屬性有幕後字段(backing field)、非私有、沒有 open
/override 或者 const 修飾符並且不是被委託的屬性,那麼你可以用 [@JvmField](https://github.com/JvmField "@JvmField") 註解該屬性。

class C(id: String) {
    @JvmField val ID = id
}
// Java
class JavaClient {
    public String getID(C c) {
        return c.ID;
    }
}

延遲初始化的屬性(在Java中)也會暴露爲字段。
該字段的可見性與 lateinit 屬性的 setter 相同。

靜態字段

在命名對象或伴生對象中聲明的 Kotlin 屬性會在該命名對象或包含伴生對象的類中
具有靜態幕後字段。

通常這些字段是私有的,但可以通過以下方式之一暴露出來:

  • [@JvmField](https://github.com/JvmField "@JvmField") 註解;
  • lateinit 修飾符;
  • const 修飾符。

使用 [@JvmField](https://github.com/JvmField "@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 字段

在命名對象或者伴生對象中的一個延遲初始化的屬性
具有與屬性 setter 相同可見性的靜態幕後字段。

object Singleton {
    lateinit var provider: Provider
}
// Java
Singleton.provider = new Provider();
// 在 Singleton 類中的 public static 非-final 字段

const 標註的(在類中以及在頂層的)屬性在 Java 中會成爲靜態字段:

// 文件 example.kt

object Obj {
    const val CONST = 1
}

class C {
    companion object {
        const val VERSION = 9
    }
}

const val MAX = 239

在 Java 中:

int c = Obj.CONST;
int d = ExampleKt.MAX;
int v = C.VERSION;

靜態方法

如上所述,Kotlin 將包級函數表示爲靜態方法。
Kotlin 還可以爲命名對象或伴生對象中定義的函數生成靜態方法,如果你將這些函數標註爲 [@JvmStatic](https://github.com/JvmStatic "@JvmStatic") 的話。
如果你使用該註解,編譯器既會在相應對象的類中生成靜態方法,也會在對象自身中生成實例方法。
例如:

class C {
    companion object {
        @JvmStatic fun foo() {}
        fun bar() {}
    }
}

現在,foo() 在 Java 中是靜態的,而 bar() 不是:

C.foo(); // 沒問題
C.bar(); // 錯誤:不是一個靜態方法
C.Companion.foo(); // 保留實例方法
C.Companion.bar(); // 唯一的工作方式

對於命名對象也同樣:

object Obj {
    @JvmStatic fun foo() {}
    fun bar() {}
}

在 Java 中:

Obj.foo(); // 沒問題
Obj.bar(); // 錯誤
Obj.INSTANCE.bar(); // 沒問題,通過單例實例調用
Obj.INSTANCE.foo(); // 也沒問題

[@JvmStatic](https://github.com/JvmStatic "@JvmStatic") 註解也可以應用於對象或伴生對象的屬性,
使其 getter 和 setter 方法在該對象或包含該伴生對象的類中是靜態成員。

可見性

Kotlin 的可見性以下列方式映射到 Java:

  • private 成員編譯成 private 成員;
  • private 的頂層聲明編譯成包級局部聲明;
  • protected 保持 protected(注意 Java 允許訪問同一個包中其他類的受保護成員,
    而 Kotlin 不能,所以 Java 類會訪問更廣泛的代碼);
  • internal 聲明會成爲 Java 中的 publicinternal 類的成員會通過名字修飾,使其
    更難以在 Java 中意外使用到,並且根據 Kotlin 規則使其允許重載相同簽名的成員
    而互不可見;
  • public 保持 public

KClass

有時你需要調用有 KClass 類型參數的 Kotlin 方法。
因爲沒有從 ClassKClass 的自動轉換,所以你必須通過調用
Class<T>.kotlin 擴展屬性的等價形式來手動進行轉換:

kotlin.jvm.JvmClassMappingKt.getKotlinClass(MainView.class)

@JvmName 解決簽名衝突

有時我們想讓一個 Kotlin 中的命名函數在字節碼中有另外一個 JVM 名稱。
最突出的例子是由於類型擦除引發的:

fun List<String>.filterValid(): List<String>
fun List<Int>.filterValid(): List<Int>

這兩個函數不能同時定義,因爲它們的 JVM 簽名是一樣的:filterValid(Ljava/util/List;)Ljava/util/List;
如果我們真的希望它們在 Kotlin 中用相同名稱,我們需要用 [@JvmName](https://github.com/JvmName "@JvmName") 去標註其中的一個(或兩個),並指定不同的名稱作爲參數:

fun List<String>.filterValid(): List<String>

@JvmName("filterValidInt")
fun List<Int>.filterValid(): List<Int>

在 Kotlin 中它們可以用相同的名稱 filterValid 來訪問,而在 Java 中,它們分別是 filterValidfilterValidInt

同樣的技巧也適用於屬性 x 和函數 getX() 共存:

val x: Int
    @JvmName("getX_prop")
    get() = 15

fun getX() = 10

生成重載

通常,如果你寫一個有默認參數值的 Kotlin 方法,在 Java 中只會有一個所有參數都存在的完整參數
簽名的方法可見,如果希望向 Java 調用者暴露多個重載,可以使用
@JvmOverloads 註解。

@JvmOverloads fun f(a: String, b: Int = 0, c: String = "abc") {
    ……
}

對於每一個有默認值的參數,都會生成一個額外的重載,這個重載會把這個參數和
它右邊的所有參數都移除掉。在上例中,會生成以下方法

// Java
void f(String a, int b, String c) { }
void f(String a, int b) { }
void f(String a) { }

該註解也適用於構造函數、靜態方法等。它不能用於抽象方法,包括
在接口中定義的方法。

請注意,如次構造函數中所述,如果一個類的所有構造函數參數都有默認
值,那麼會爲其生成一個公有的無參構造函數。這就算
沒有 @JvmOverloads 註解也有效。

受檢異常

如上所述,Kotlin 沒有受檢異常。
所以,通常 Kotlin 函數的 Java 簽名不會聲明拋出異常。
於是如果我們有一個這樣的 Kotlin 函數:

// example.kt
package demo

fun foo() {
    throw IOException()
}

然後我們想要在 Java 中調用它並捕捉這個異常:

// Java
try {
  demo.Example.foo();
}
catch (IOException e) { // 錯誤:foo() 未在 throws 列表中聲明 IOException
  // ……
}

因爲 foo() 沒有聲明 IOException,我們從 Java 編譯器得到了一個報錯消息。
爲了解決這個問題,要在 Kotlin 中使用 [@Throws](https://github.com/Throws "@Throws") 註解。

@Throws(IOException::class)
fun foo() {
    throw IOException()
}

空安全性

當從 Java 中調用 Kotlin 函數時,沒人阻止我們將 null{: .keyword } 作爲非空參數傳遞。
這就是爲什麼 Kotlin 給所有期望非空參數的公有函數生成運行時檢測。
這樣我們就能在 Java 代碼裏立即得到 NullPointerException

型變的泛型

當 Kotlin 的類使用了聲明處型變,有兩種選擇
可以從 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("s")),但是在 Java 中是行不通的,因爲在 Java 中
Box 在其泛型參數 T 上是不型變的,於是 Box<Derived> 並不是 Box<Base> 的子類。
要使其在 Java 中工作,我們按以下這樣定義 unboxBase

Base unboxBase(Box<? extends Base> box) { …… }

這裏我們使用 Java 的通配符類型(? extends Base)來
通過使用處型變來模擬聲明處型變,因爲在 Java 中只能這樣。

當它作爲參數出現時,爲了讓 Kotlin 的 API 在 Java 中工作,對於協變定義的 Box 我們生成 Box<Super> 作爲 Box<? extends Super>
(或者對於逆變定義的 Foo 生成 Foo<? super Bar>)。當它是一個返回值時,
我們不生成通配符,因爲否則 Java 客戶端將必須處理它們(並且它違反常用
Java 編碼風格)。因此,我們的示例中的對應函數實際上翻譯如下:

// 作爲返回類型——沒有通配符
Box<Derived> boxDerived(Derived value) { …… }

// 作爲參數——有通配符
Base unboxBase(Box<? extends Base> box) { …… }

注意:當參數類型是 final 時,生成通配符通常沒有意義,所以無論在什麼地方 Box<String>
始終轉換爲 Box<String>

如果我們在默認不生成通配符的地方需要通配符,我們可以使用 [@JvmWildcard](https://github.com/JvmWildcard "@JvmWildcard") 註解:

fun boxDerived(value: Derived): Box<@JvmWildcard Derived> = Box(value)
// 將被轉換成
// Box<? extends Derived> boxDerived(Derived value) { …… }

另一方面,如果我們根本不需要默認的通配符轉換,我們可以使用[@JvmSuppressWildcards](https://github.com/JvmSuppressWildcards "@JvmSuppressWildcards")

fun unboxBase(box: Box<@JvmSuppressWildcards Base>): Base = box.value
// 會翻譯成
// Base unboxBase(Box<Base> box) { …… }

注意:[@JvmSuppressWildcards](https://github.com/JvmSuppressWildcards "@JvmSuppressWildcards") 不僅可用於單個類型參數,還可用於整個聲明(如
函數或類),從而抑制其中的所有通配符。

Nothing 類型翻譯

類型 Nothing 是特殊的,因爲它在 Java 中沒有自然的對應。確實,每個 Java 引用類型,包括
java.lang.Void 都可以接受 null 值,但是 Nothing 不行。因此,這種類型不能在 Java 世界中
準確表示。這就是爲什麼在使用 Nothing 參數的地方 Kotlin 生成一個原始類型:

fun emptyList(): List<Nothing> = listOf()
// 會翻譯成
// List emptyList() { …… }