從 Java 呼叫 Kotlin
Kotlin 程式碼可以輕鬆地從 Java 中呼叫。 例如,Kotlin 類別的實例可以在 Java 方法中無縫地建立和操作。 然而,Java 和 Kotlin 之間存在一些差異,在將 Kotlin 程式碼整合到 Java 時需要注意。 在此頁面上,我們將描述如何調整 Kotlin 程式碼與其 Java 用戶端的互通性。
屬性
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
。
套件級別函數
在 app.kt
檔案中、org.example
套件內宣告的所有函數和屬性,包括擴充函數,都會被編譯成名為 org.example.AppKt
的 Java 類別的靜態方法。
// app.kt
package org.example
class Util
fun getTime() { /*...*/ }
// Java
new org.example.Util();
org.example.AppKt.getTime();
若要為生成的 Java 類別設定自訂名稱,請使用 @JvmName
註解:
@file:JvmName("DemoUtils")
package org.example
class Util
fun getTime() { /*...*/ }
// Java
new org.example.Util();
org.example.DemoUtils.getTime();
多個檔案具有相同的生成 Java 類別名稱(相同的套件和相同的名稱,或相同的 @JvmName
註解)通常會導致錯誤。 然而,編譯器可以生成一個單一的 Java facade 類別,該類別具有指定的名稱,並包含所有具有該名稱的檔案中的所有宣告。 若要啟用此類 facade 的生成,請在所有此類檔案中使用 @JvmMultifileClass
註解。
// oldutils.kt
@file:JvmName("Utils")
@file:JvmMultifileClass
package org.example
fun getTime() { /*...*/ }
// newutils.kt
@file:JvmName("Utils")
@file:JvmMultifileClass
package org.example
fun getDate() { /*...*/ }
// Java
org.example.Utils.getTime();
org.example.Utils.getDate();
實例欄位
如果您需要將 Kotlin 屬性作為 Java 中的欄位公開,請使用 @JvmField
註解對其進行註解。 該欄位具有與底層屬性相同的可見性。您可以對符合以下條件的屬性使用 @JvmField
註解:
- 具有支援欄位
- 不是 private
- 沒有
open
、override
或const
修飾符 - 不是委派屬性
class User(id: String) {
@JvmField val ID = id
}
// Java
class JavaClient {
public String getID(User user) {
return user.ID;
}
}
延遲初始化 的屬性也會作為欄位公開。 該欄位的可見性與 lateinit
屬性 setter 的可見性相同。
靜態欄位
在具名物件或伴生物件中宣告的 Kotlin 屬性,會在其具名物件或包含伴生物件的類別中具有靜態支援欄位。
通常這些欄位是 private 的,但可以透過以下方式之一公開:
@JvmField
註解lateinit
修飾符const
修飾符
使用 @JvmField
註解標記此類屬性,使其成為具有與屬性本身相同可見性的靜態欄位。
class Key(val value: Int) {
companion object {
@JvmField
val COMPARATOR: Comparator<Key> = compareBy<Key> { it.value }
}
}
// Java
Key.COMPARATOR.compare(key1, key2);
// public static final field in Key class
物件或伴生物件中的 延遲初始化 屬性,具有與屬性 setter 相同可見性的靜態支援欄位。
object Singleton {
lateinit var provider: Provider
}
// Java
Singleton.provider = new Provider();
// public static non-final field in Singleton class
宣告為 const
的屬性(無論是在類別中還是在頂層)在 Java 中都會轉變為靜態欄位:
// file example.kt
object Obj {
const val CONST = 1
}
class C {
companion object {
const val VERSION = 9
}
}
const val MAX = 239
在 Java 中:
int constant = Obj.CONST;
int max = ExampleKt.MAX;
int version = C.VERSION;
靜態方法
如前所述,Kotlin 將套件級別的函數表示為靜態方法。 如果您將在具名物件或伴生物件中定義的函數註解為 @JvmStatic
,Kotlin 也可以為它們生成靜態方法。 如果您使用此註解,編譯器將在物件的封裝類別中生成一個靜態方法,並在物件本身中生成一個實例方法。例如:
class C {
companion object {
@JvmStatic fun callStatic() {}
fun callNonStatic() {}
}
}
現在,callStatic()
在 Java 中是靜態的,而 callNonStatic()
則不是:
C.callStatic(); // works fine
C.callNonStatic(); // error: not a static method
C.Companion.callStatic(); // instance method remains
C.Companion.callNonStatic(); // the only way it works
同樣地,對於具名物件:
object Obj {
@JvmStatic fun callStatic() {}
fun callNonStatic() {}
}
在 Java 中:
Obj.callStatic(); // works fine
Obj.callNonStatic(); // error
Obj.INSTANCE.callNonStatic(); // works, a call through the singleton instance
Obj.INSTANCE.callStatic(); // works too
從 Kotlin 1.3 開始,@JvmStatic
也適用於介面伴生物件中定義的函數。 此類函數會編譯為介面中的靜態方法。請注意,介面中的靜態方法是在 Java 1.8 中引入的,因此請務必使用對應的目標。
interface ChatBot {
companion object {
@JvmStatic fun greet(username: String) {
println("Hello, $username")
}
}
}
您也可以將 @JvmStatic
註解應用於物件或伴生物件的屬性,使其 getter 和 setter 方法成為該物件或包含伴生物件的類別中的靜態成員。
介面中的預設方法
當目標為 JVM 時,Kotlin 會將介面中宣告的函數編譯為 預設方法,除非 另行配置。 這些是介面中的具體方法,Java 類別可以直接繼承,無需重新實作。
以下是一個帶有預設方法的 Kotlin 介面範例:
interface Robot {
fun move() { println("~walking~") } // will be default in the Java interface
fun speak(): Unit
}
預設實作可用於實作該介面的 Java 類別。
//Java implementation
public class C3PO implements Robot {
// move() implementation from Robot is available implicitly
@Override
public void speak() {
System.out.println("I beg your pardon, sir");
}
}
C3PO c3po = new C3PO();
c3po.move(); // default implementation from the Robot interface
c3po.speak();
介面的實作可以覆寫預設方法。
//Java
public class BB8 implements Robot {
//own implementation of the default method
@Override
public void move() {
System.out.println("~rolling~");
}
@Override
public void speak() {
System.out.println("Beep-beep");
}
}
預設方法的相容性模式
Kotlin 提供了三種模式來控制介面中的函數如何編譯為 JVM 預設方法。 這些模式決定了編譯器是否會生成相容性橋接器和 DefaultImpls
類別中的靜態方法。
您可以使用 -jvm-default
編譯器選項來控制此行為:
-jvm-default
編譯器選項取代了已棄用的-Xjvm-default
選項。
了解更多關於相容性模式:
enable
預設行為。 在介面中生成預設實作,並包含相容性橋接器和 DefaultImpls
類別。 此模式保持與舊版已編譯 Kotlin 程式碼的相容性。
no-compatibility
僅在介面中生成預設實作。 跳過相容性橋接器和 DefaultImpls
類別。 將此模式用於不與依賴 DefaultImpls
類別的程式碼互動的新程式碼庫。 這可能會破壞與舊版 Kotlin 程式碼的二進位制相容性。
如果使用介面委派,所有介面方法都會被委派。
disable
禁用介面中的預設實作。 僅生成相容性橋接器和 DefaultImpls
類別。
可見性
Kotlin 的可見性修飾符與 Java 的對應方式如下:
private
成員會被編譯成private
成員。private
頂層宣告會被編譯成private
頂層宣告。如果從類別內部存取,也會包含套件私有存取器。protected
保持protected
。(請注意,Java 允許從同一套件中的其他類別存取 protected 成員,而 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
註解,並將不同的名稱指定為參數:
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
若要變更未明確實作 getter 和 setter 的屬性所生成的存取器方法的名稱,您可以使用 @get:JvmName
和 @set:JvmName
:
@get:JvmName("x")
@set:JvmName("changeX")
var x: Int = 23
重載生成
通常,如果您編寫一個帶有預設參數值的 Kotlin 函數,它在 Java 中只能以完整簽名的形式可見,所有參數都必須存在。如果您希望向 Java 呼叫者公開多個重載,可以使用 @JvmOverloads
註解。
此註解也適用於建構函數、靜態方法等等。它不能用於抽象方法,包括在介面中定義的方法。
class Circle @JvmOverloads constructor(centerX: Int, centerY: Int, radius: Double = 1.0) {
@JvmOverloads fun draw(label: String, lineWidth: Int = 1, color: String = "red") { /*...*/ }
}
對於每個帶有預設值的參數,這會生成一個額外的重載,其中移除了此參數以及參數列表右側的所有參數。在此範例中,生成了以下內容:
// Constructors:
Circle(int centerX, int centerY, double radius)
Circle(int centerX, int centerY)
// Methods
void draw(String label, int lineWidth, String color) { }
void draw(String label, int lineWidth) { }
void draw(String label) { }
請注意,如 次要建構函數 中所述,如果一個類別的所有建構函數參數都具有預設值,則會為其生成一個無參數的 public 建構函數。這即使未指定 @JvmOverloads
註解也有效。
受檢異常
Kotlin 沒有受檢異常。 因此,通常 Kotlin 函數的 Java 簽名不宣告拋出的異常。 因此,如果您有這樣一個 Kotlin 函數:
// example.kt
package demo
fun writeToFile() {
/*...*/
throw IOException()
}
而您想從 Java 中呼叫它並捕獲異常:
// Java
try {
demo.Example.writeToFile();
} catch (IOException e) {
// error: writeToFile() does not declare IOException in the throws list
// ...
}
您會收到來自 Java 編譯器的錯誤訊息,因為 writeToFile()
未宣告 IOException
。 為了解決這個問題,請在 Kotlin 中使用 @Throws
註解:
@Throws(IOException::class)
fun writeToFile() {
/*...*/
throw IOException()
}
空安全
從 Java 呼叫 Kotlin 函數時,沒有什麼能阻止我們將 null
作為非空參數傳遞。 這就是為什麼 Kotlin 會為所有期望非空的 public 函數生成運行時檢查。 這樣我們就會立即在 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(Derived()))
,但在 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>
(或逆變定義的 Foo
的 Foo<? super Bar>
)生成為 Box<? extends Super>
,當它作為 參數 出現時。當它是回傳值時,不會生成萬用字元,因為否則 Java 客戶端將不得不處理它們(這與常見的 Java 編碼風格相悖)。因此,我們範例中的函數實際上會被翻譯成以下內容:
// return type - no wildcards
Box<Derived> boxDerived(Derived value) { ... }
// parameter - wildcards
Base unboxBase(Box<? extends Base> box) { ... }
當引數類型是 final 時,通常沒有必要生成萬用字元,因此
Box<String>
始終是Box<String>
,無論它處於何種位置。
如果您在預設情況下不生成萬用字元的地方需要萬用字元,請使用 @JvmWildcard
註解:
fun boxDerived(value: Derived): Box<@JvmWildcard Derived> = Box(value)
// is translated to
// Box<? extends Derived> boxDerived(Derived value) { ... }
在相反的情況下,如果您在生成萬用字元的地方不需要萬用字元,請使用 @JvmSuppressWildcards
:
fun unboxBase(box: Box<@JvmSuppressWildcards Base>): Base = box.value
// is translated to
// Base unboxBase(Box<Base> box) { ... }
@JvmSuppressWildcards
不僅可以用於單個類型引數,還可以用於整個宣告,例如函數或類別,導致其中所有的萬用字元都被抑制。
Nothing 類型的翻譯
類型 Nothing
很特殊,因為它在 Java 中沒有天然的對應物。確實,每個 Java 引用類型,包括 java.lang.Void
,都接受 null
作為值,而 Nothing
甚至不接受 null
。因此,這種類型無法在 Java 世界中被精確表示。這就是為什麼當使用 Nothing
類型的引數時,Kotlin 會生成一個原始類型:
fun emptyList(): List<Nothing> = listOf()
// is translated to
// List emptyList() { ... }
內聯值類別
如果您希望 Java 程式碼能與 Kotlin 的 內聯值類別 順暢協作,您可以使用 @JvmExposeBoxed
註解或 -Xjvm-expose-boxed
編譯器選項。這些方法確保 Kotlin 生成 Java 互通性所需的裝箱表示。
預設情況下,Kotlin 會將內聯值類別編譯為使用未裝箱表示,這通常無法從 Java 存取。 例如,您無法從 Java 呼叫 MyInt
類別的建構函數:
@JvmInline
value class MyInt(val value: Int)
因此以下 Java 程式碼會失敗:
MyInt input = new MyInt(5);
您可以使用 @JvmExposeBoxed
註解,以便 Kotlin 生成一個您可以從 Java 直接呼叫的 public 建構函數。 您可以將此註解應用於以下層級,以確保對公開給 Java 的內容進行細粒度控制:
- 類別
- 建構函數
- 函數
在程式碼中使用 @JvmExposeBoxed
註解之前,您必須透過使用 @OptIn(ExperimentalStdlibApi::class)
來選擇啟用。 例如:
@OptIn(ExperimentalStdlibApi::class)
@JvmExposeBoxed
@JvmInline
value class MyInt(val value: Int)
@OptIn(ExperimentalStdlibApi::class)
@JvmExposeBoxed
fun MyInt.timesTwoBoxed(): MyInt = MyInt(this.value * 2)
有了這些註解,Kotlin 會為 MyInt
類別生成一個 Java 可存取的建構函數,以及一個使用值類別裝箱形式的擴充函數變體。因此以下 Java 程式碼會成功運行:
MyInt input = new MyInt(5);
MyInt output = ExampleKt.timesTwoBoxed(input);
若要將此行為應用於模組內的所有內聯值類別以及使用它們的函數,請使用 -Xjvm-expose-boxed
選項進行編譯。 使用此選項進行編譯的效果,等同於模組中的每個宣告都具有 @JvmExposeBoxed
註解。
繼承的函數
@JvmExposeBoxed
註解不會自動為繼承的函數生成裝箱表示。
若要為繼承的函數生成必要的表示,請在實作或擴充類別中覆寫它:
interface IdTransformer {
fun transformId(rawId: UInt): UInt = rawId
}
// Doesn't generate a boxed representation for the transformId() function
@OptIn(ExperimentalStdlibApi::class)
@JvmExposeBoxed
class LightweightTransformer : IdTransformer
// Generates a boxed representation for the transformId() function
@OptIn(ExperimentalStdlibApi::class)
@JvmExposeBoxed
class DefaultTransformer : IdTransformer {
override fun transformId(rawId: UInt): UInt = super.transformId(rawId)
}
若要了解 Kotlin 中的繼承如何運作以及如何使用 super
關鍵字呼叫超類別實作,請參閱 繼承。