在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 中的public
。internal
類的成員會通過名字修飾,使其
更難以在 Java 中意外使用到,並且根據 Kotlin 規則使其允許重載相同簽名的成員
而互不可見;-
public
保持public
。
KClass
有時你需要調用有 KClass
類型參數的 Kotlin 方法。
因爲沒有從 Class
到 KClass
的自動轉換,所以你必須通過調用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 中,它們分別是 filterValid
和 filterValidInt
。
同樣的技巧也適用於屬性 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() { …… }