Java 可以輕松調(diào)用 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 開頭,則使用不同的名稱映射規(guī)則:getter 的名稱
與屬性名稱相同,并且 setter 的名稱是通過將 is 替換為 set 獲得。
例如,對于屬性 isOpen,其 getter 會稱做 isOpen(),而其 setter 會稱做 setOpen()。
這一規(guī)則適用于任何類型的屬性,并不僅限于 Boolean。
在 org.foo.bar 包內(nèi)的 example.kt 文件中聲明的所有的函數(shù)和屬性,包括擴展函數(shù),
都編譯成一個名為 org.foo.bar.ExampleKt 的 Java 類的靜態(tài)方法。
// example.kt
package demo
class Foo
fun bar() {
}
// Java
new demo.Foo();
demo.ExampleKt.bar();
可以使用 @JvmName 注解修改生成的 Java 類的類名:
@file:JvmName("DemoUtils")
package demo
class Foo
fun bar() {
}
// Java
new demo.Foo();
demo.DemoUtils.bar();
如果多個文件中生成了相同的 Java 類名(包名相同并且類名相同或者有相同的@JvmName 注解)通常是錯誤的。然而,編譯器能夠生成一個單一的 Java 外觀
類,它具有指定的名稱且包含來自所有文件中具有該名稱的所有聲明。
要啟用生成這樣的外觀,請在所有相關(guān)文件中使用 @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 注解對其標注。
該字段將具有與底層屬性相同的可見性。如果一個屬性有幕后字段(backing field)、非私有、沒有 open
/override 或者 const 修飾符并且不是被委托的屬性,那么你可以用 @JvmField 注解該屬性。
class C(id: String) {
@JvmField val ID = id
}
// Java
class JavaClient {
public String getID(C c) {
return c.ID;
}
}
延遲初始化的屬性(在Java中)也會暴露為字段。
該字段的可見性與 lateinit 屬性的 setter 相同。
在命名對象或伴生對象中聲明的 Kotlin 屬性會在該命名對象或包含伴生對象的類中
具有靜態(tài)幕后字段。
通常這些字段是私有的,但可以通過以下方式之一暴露出來:
@JvmField 注解;lateinit 修飾符;const 修飾符。使用 @JvmField 標注這樣的屬性使其成為與屬性本身具有相同可見性的靜態(tài)字段。
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 相同可見性的靜態(tài)幕后字段。
object Singleton {
lateinit var provider: Provider
}
// Java
Singleton.provider = new Provider();
// 在 Singleton 類中的 public static 非-final 字段
用 const 標注的(在類中以及在頂層的)屬性在 Java 中會成為靜態(tài)字段:
// 文件 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 將包級函數(shù)表示為靜態(tài)方法。
Kotlin 還可以為命名對象或伴生對象中定義的函數(shù)生成靜態(tài)方法,如果你將這些函數(shù)標注為 @JvmStatic 的話。
如果你使用該注解,編譯器既會在相應(yīng)對象的類中生成靜態(tài)方法,也會在對象自身中生成實例方法。
例如:
class C {
companion object {
@JvmStatic fun foo() {}
fun bar() {}
}
}
現(xiàn)在,foo() 在 Java 中是靜態(tài)的,而 bar() 不是:
C.foo(); // 沒問題
C.bar(); // 錯誤:不是一個靜態(tài)方法
C.Companion.foo(); // 保留實例方法
C.Companion.bar(); // 唯一的工作方式
對于命名對象也同樣:
object Obj {
@JvmStatic fun foo() {}
fun bar() {}
}
在 Java 中:
Obj.foo(); // 沒問題
Obj.bar(); // 錯誤
Obj.INSTANCE.bar(); // 沒問題,通過單例實例調(diào)用
Obj.INSTANCE.foo(); // 也沒問題
@JvmStatic 注解也可以應(yīng)用于對象或伴生對象的屬性,
使其 getter 和 setter 方法在該對象或包含該伴生對象的類中是靜態(tài)成員。
Kotlin 的可見性以下列方式映射到 Java:
private 成員編譯成 private 成員;private 的頂層聲明編譯成包級局部聲明;protected 保持 protected(注意 Java 允許訪問同一個包中其他類的受保護成員,internal 聲明會成為 Java 中的 public。internal 類的成員會通過名字修飾,使其public 保持 public。有時你需要調(diào)用有 KClass 類型參數(shù)的 Kotlin 方法。
因為沒有從 Class 到 KClass 的自動轉(zhuǎn)換,所以你必須通過調(diào)用Class<T>.kotlin 擴展屬性的等價形式來手動進行轉(zhuǎn)換:
kotlin.jvm.JvmClassMappingKt.getKotlinClass(MainView.class)
有時我們想讓一個 Kotlin 中的命名函數(shù)在字節(jié)碼中有另外一個 JVM 名稱。
最突出的例子是由于類型擦除引發(fā)的:
fun List<String>.filterValid(): List<String>
fun List<Int>.filterValid(): List<Int>
這兩個函數(shù)不能同時定義,因為它們的 JVM 簽名是一樣的:filterValid(Ljava/util/List;)Ljava/util/List;。
如果我們真的希望它們在 Kotlin 中用相同名稱,我們需要用 @JvmName 去標注其中的一個(或兩個),并指定不同的名稱作為參數(shù):
fun List<String>.filterValid(): List<String>
@JvmName("filterValidInt")
fun List<Int>.filterValid(): List<Int>
在 Kotlin 中它們可以用相同的名稱 filterValid 來訪問,而在 Java 中,它們分別是 filterValid 和 filterValidInt。
同樣的技巧也適用于屬性 x 和函數(shù) getX() 共存:
val x: Int
@JvmName("getX_prop")
get() = 15
fun getX() = 10
通常,如果你寫一個有默認參數(shù)值的 Kotlin 方法,在 Java 中只會有一個所有參數(shù)都存在的完整參數(shù)
簽名的方法可見,如果希望向 Java 調(diào)用者暴露多個重載,可以使用
@JvmOverloads 注解。
@JvmOverloads fun f(a: String, b: Int = 0, c: String = "abc") {
……
}
對于每一個有默認值的參數(shù),都會生成一個額外的重載,這個重載會把這個參數(shù)和
它右邊的所有參數(shù)都移除掉。在上例中,會生成以下方法
:
// Java
void f(String a, int b, String c) { }
void f(String a, int b) { }
void f(String a) { }
該注解也適用于構(gòu)造函數(shù)、靜態(tài)方法等。它不能用于抽象方法,包括
在接口中定義的方法。
請注意,如次構(gòu)造函數(shù)中所述,如果一個類的所有構(gòu)造函數(shù)參數(shù)都有默認
值,那么會為其生成一個公有的無參構(gòu)造函數(shù)。這就算
沒有 @JvmOverloads 注解也有效。
如上所述,Kotlin 沒有受檢異常。
所以,通常 Kotlin 函數(shù)的 Java 簽名不會聲明拋出異常。
于是如果我們有一個這樣的 Kotlin 函數(shù):
// example.kt
package demo
fun foo() {
throw IOException()
}
然后我們想要在 Java 中調(diào)用它并捕捉這個異常:
// Java
try {
demo.Example.foo();
}
catch (IOException e) { // 錯誤:foo() 未在 throws 列表中聲明 IOException
// ……
}
因為 foo() 沒有聲明 IOException,我們從 Java 編譯器得到了一個報錯消息。
為了解決這個問題,要在 Kotlin 中使用 @Throws 注解。
@Throws(IOException::class)
fun foo() {
throw IOException()
}
當從 Java 中調(diào)用 Kotlin 函數(shù)時,沒人阻止我們將 null{: .keyword } 作為非空參數(shù)傳遞。
這就是為什么 Kotlin 給所有期望非空參數(shù)的公有函數(shù)生成運行時檢測。
這樣我們就能在 Java 代碼里立即得到 NullPointerException。
當 Kotlin 的類使用了聲明處型變,有兩種選擇
可以從 Java 代碼中看到它們的用法。讓我們假設(shè)我們有以下類和兩個使用它的函數(shù):
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
一種看似理所當然地將這倆函數(shù)轉(zhuǎn)換成 Java 代碼的方式可能會是:
Box<Derived> boxDerived(Derived value) { …… }
Base unboxBase(Box<Base> box) { …… }
問題是,在 Kotlin 中我們可以這樣寫 unboxBase(boxDerived("s")),但是在 Java 中是行不通的,因為在 Java 中
類 Box 在其泛型參數(shù) T 上是不型變的,于是 Box<Derived> 并不是 Box<Base> 的子類。
要使其在 Java 中工作,我們按以下這樣定義 unboxBase:
Base unboxBase(Box<? extends Base> box) { …… }
這里我們使用 Java 的通配符類型(? extends Base)來
通過使用處型變來模擬聲明處型變,因為在 Java 中只能這樣。
當它作為參數(shù)出現(xiàn)時,為了讓 Kotlin 的 API 在 Java 中工作,對于協(xié)變定義的 Box 我們生成 Box<Super> 作為 Box<? extends Super>
(或者對于逆變定義的 Foo 生成 Foo<? super Bar>)。當它是一個返回值時,
我們不生成通配符,因為否則 Java 客戶端將必須處理它們(并且它違反常用
Java 編碼風(fēng)格)。因此,我們的示例中的對應(yīng)函數(shù)實際上翻譯如下:
// 作為返回類型——沒有通配符
Box<Derived> boxDerived(Derived value) { …… }
// 作為參數(shù)——有通配符
Base unboxBase(Box<? extends Base> box) { …… }
注意:當參數(shù)類型是 final 時,生成通配符通常沒有意義,所以無論在什么地方 Box<String>
始終轉(zhuǎn)換為 Box<String>。
如果我們在默認不生成通配符的地方需要通配符,我們可以使用 @JvmWildcard 注解:
fun boxDerived(value: Derived): Box<@JvmWildcard Derived> = Box(value)
// 將被轉(zhuǎn)換成
// Box<? extends Derived> boxDerived(Derived value) { …… }
另一方面,如果我們根本不需要默認的通配符轉(zhuǎn)換,我們可以使用@JvmSuppressWildcards
fun unboxBase(box: Box<@JvmSuppressWildcards Base>): Base = box.value
// 會翻譯成
// Base unboxBase(Box<Base> box) { …… }
注意:@JvmSuppressWildcards 不僅可用于單個類型參數(shù),還可用于整個聲明(如
函數(shù)或類),從而抑制其中的所有通配符。
類型 Nothing 是特殊的,因為它在 Java 中沒有自然的對應(yīng)。確實,每個 Java 引用類型,包括java.lang.Void 都可以接受 null 值,但是 Nothing 不行。因此,這種類型不能在 Java 世界中
準確表示。這就是為什么在使用 Nothing 參數(shù)的地方 Kotlin 生成一個原始類型:
fun emptyList(): List<Nothing> = listOf()
// 會翻譯成
// List emptyList() { …… }