本文件說明一系列在 Java 和 Kotlin 中編寫公用 API 的規則,目的是讓程式碼在以其他語言使用時符合該語言的習慣。
Java (搭配 Kotlin)
沒有硬關鍵字
請勿將 Kotlin 的任何硬關鍵字做為方法或欄位的名稱。從 Kotlin 呼叫時,這些變數需要使用倒引號來逸出。可使用軟關鍵字、修飾詞關鍵字和特殊 ID。
例如,從 Kotlin 呼叫時,Mockito 的 when
函式需要使用倒引號:
val callable = Mockito.mock(Callable::class.java) Mockito.`when`(callable.call()).thenReturn(/* … */)
避免使用 Any
擴充功能名稱
除非絕對必要,否則避免將 Any
上的擴充功能函式名稱用於方法,或將 Any
上的擴充功能屬性名稱用於欄位。雖然成員方法和欄位始終優先於 Any
的擴充功能函式或屬性,但在讀取程式碼時,可能難以辨識受到呼叫的方法或欄位。
是否可為空值註解
公開 API 中的每個非原始參數、回傳和欄位類型,都應該有是否可為空值註解。無註解的類型會被解釋為「平台」類型。這些類型具有不明確的是否可為空值屬性。
根據預設,Kotlin 編譯器旗標會採用 JSR 305 註解,但會以警示做標記。您也可以設定旗標,讓編譯器將註解視為錯誤。
上次 Lambda 參數
符合 SAM 轉換資格的參數類型應為上次的類型。
例如,RxJava 2’s Flowable.create()
方法簽名的定義為:
public staticFlowable create( FlowableOnSubscribe source, BackpressureStrategy mode) { /* … */ }
由於 FlowableOnSubscribe 符合 SAM 轉換的資格,以這種方法從 Kotlin 執行的函式呼叫為:
Flowable.create({ /* … */ }, BackpressureStrategy.LATEST)
但是,如果方法簽名中的參數遭到撤銷,則函式呼叫可以使用結尾的 lambda 語法:
Flowable.create(BackpressureStrategy.LATEST) { /* … */ }
屬性前置字元
如要在 Kotlin 中以屬性形式表示方法,則必須使用嚴格的「bean」式前置字元。
存取子方法需要「get」前置字元。如果採用布林值「傳回」方法,則可使用「is」前置字元。
public final class User { public String getName() { /* … */ } public boolean isActive() { /* … */ } }
val name = user.name // Invokes user.getName() val active = user.isActive // Invokes user.isActive()
關聯的更動子方法需要「set」前置字元。
public final class User { public String getName() { /* … */ } public void setName(String name) { /* … */ } public boolean isActive() { /* … */ } public void setActive(boolean active) { /* … */ } }
user.name = "Bob" // Invokes user.setName(String) user.isActive = true // Invokes user.setActive(boolean)
如果您想採用以屬性形式公開的方法,請勿使用非標準前置字元,例如「has」/「set」或非「get」前置字元的存取子。包含非標準前置字元的方法仍按函式來呼叫,但須視方法的行為而定。
運算子超載
請特別留意允許 Kotlin 特殊呼叫網站語法 (即運算子超載) 的方法名稱。請確保這類方法名稱能與縮短的語法搭配使用。
public final class IntBox { private final int value; public IntBox(int value) { this.value = value; } public IntBox plus(IntBox other) { return new IntBox(value + other.value); } }
val one = IntBox(1) val two = IntBox(2) val three = one + two // Invokes one.plus(two)
Kotlin (搭配 Java)
檔案名稱
如果檔案包含頂層函式或屬性,請「一律」在註解中加入 @file:JvmName("Foo")
來正確命名。
根據預設,「MyClass.kt」檔案中的頂層成員會隸屬於名為「MyClassKt
」的類別。該類別缺乏吸引力,並洩露做為實作詳細資料的語言。
請考慮新增 @file:JvmMultifileClass
,將多個檔案的頂層成員合併為單一類別。
Lambda 引數
要從 Java 使用的函式類型應避免傳回類型 Unit
。但前提是必須指定不符合語言習慣的明確 return
Unit.INSTANCE;
陳述式。
fun sayHi(callback: (String) -> Unit) = /* … */
// Kotlin caller: greeter.sayHi { Log.d("Greeting", "Hello, $it!") }
// Java caller: greeter.sayHi(name -> { Log.d("Greeting", "Hello, " + name + "!"); return Unit.INSTANCE; });
這個語法也不支援依語意命名的類型,因此能夠在其他類型上實作。
在 Kotlin 中,為 lambda 類型定義已命名的單抽象方法 (SAM) 介面,即可修正 Java 的問題,但會妨礙在 Kotlin 使用 lambda 語法。
interface GreeterCallback { fun greetName(name: String): Unit } fun sayHi(callback: GreeterCallback) = /* … */
// Kotlin caller: greeter.sayHi(object : GreeterCallback { override fun greetName(name: String) { Log.d("Greeting", "Hello, $name!") } })
// Java caller: greeter.sayHi(name -> Log.d("Greeting", "Hello, " + name + "!"))
在 Java 中定義已命名的 SAM 介面,可讓您使用略遜一籌的 Kotlin lambda 語法,但必須在該語法內明確指定介面類型。
// Defined in Java: interface GreeterCallback { void greetName(String name); }
fun sayHi(greeter: GreeterCallback) = /* … */
// Kotlin caller: greeter.sayHi(GreeterCallback { Log.d("Greeting", "Hello, $it!") })
// Java caller: greeter.sayHi(name -> Log.d("Greeter", "Hello, " + name + "!"));
目前,沒有方法可以定義做為 lambda 而搭配 Java 和 Kotlin 使用的參數類型,所以也無法使這樣的類型符合兩種語言的習慣。目前建議的類型為函式類型,但傳回類型為 Unit
時,Java 的體驗品質較低。
避免使用 Nothing
泛型
一般參數為 Nothing
的類型,會以原始類型的形式對 Java 公開。Java 中很少使用原始類型,也應避免使用。
文件例外狀況
擲回已檢查例外狀況的函式,應使用 @Throws
來記錄這些例外狀況。執行階段例外狀況必須記載在 KDoc 內。
請注意函式委派的 API,因為這些 API 可能會擲回已檢查的例外狀況,也就是 Kotlin 自動允許散布的例外情況。
防禦型副本
從共用 API 傳回共用或未擁有的唯讀集合時,請將這些集合納入無法修改的容器中,或執行防禦型副本。儘管 Kotlin 會強制執行唯讀屬性,但 Java 不會強制執行。如未使用包裝函式或防禦型副本,則可傳回長效集合參照資料來打破這些不變量。
夥伴函式
夥伴物件中的公開函式必須以 @JvmStatic
加註,以便以靜態方法公開。
如果沒有註解,這些函式僅可在靜態 Companion
欄位上用做執行個體方法。
「錯誤:無註解」
class KotlinClass { companion object { fun doWork() { /* … */ } } }
public final class JavaClass { public static void main(String... args) { KotlinClass.Companion.doWork(); } }
「正確:@JvmStatic
註解」
class KotlinClass { companion object { @JvmStatic fun doWork() { /* … */ } } }
public final class JavaClass { public static void main(String... args) { KotlinClass.doWork(); } }
夥伴常數
公開的非 const
屬性如果是 companion object
中的有效常數,則必須使用 @JvmField
加註,才能以靜態欄位的形式公開。
如果沒有註解,這些屬性僅可在靜態 Companion
欄位上用做使用奇怪名稱的「getter」執行個體。使用 @JvmStatic
而不是 @JvmField
,可將使用奇怪名稱的「getter」移至類別上的靜態方法,但這樣仍不正確。
「錯誤:無註解」
class KotlinClass { companion object { const val INTEGER_ONE = 1 val BIG_INTEGER_ONE = BigInteger.ONE } }
public final class JavaClass { public static void main(String... args) { System.out.println(KotlinClass.INTEGER_ONE); System.out.println(KotlinClass.Companion.getBIG_INTEGER_ONE()); } }
「錯誤:@JvmStatic
註解」
class KotlinClass { companion object { const val INTEGER_ONE = 1 @JvmStatic val BIG_INTEGER_ONE = BigInteger.ONE } }
public final class JavaClass { public static void main(String... args) { System.out.println(KotlinClass.INTEGER_ONE); System.out.println(KotlinClass.getBIG_INTEGER_ONE()); } }
「正確:@JvmField
註解」
class KotlinClass { companion object { const val INTEGER_ONE = 1 @JvmField val BIG_INTEGER_ONE = BigInteger.ONE } }
public final class JavaClass { public static void main(String... args) { System.out.println(KotlinClass.INTEGER_ONE); System.out.println(KotlinClass.BIG_INTEGER_ONE); } }
慣用命名方式
Kotlin 與 Java 的呼叫慣例不同,因此您須改變函式的命名方式。請使用 @JvmName
來設計名稱,讓它們符合這兩種語言的慣例,或符合這兩種語言各自的標準程式庫命名方式。
這種情況最常在擴充功能函式和擴充功能屬性中發生,因為接收器類型的位置不同。
sealed class Optionaldata class Some (val value: T): Optional () object None : Optional () @JvmName("ofNullable") fun T?.asOptional() = if (this == null) None else Some(this)
// FROM KOTLIN: fun main(vararg args: String) { val nullableString: String? = "foo" val optionalString = nullableString.asOptional() }
// FROM JAVA: public static void main(String... args) { String nullableString = "Foo"; OptionaloptionalString = Optionals.ofNullable(nullableString); }
預設函式超載
函式如果使用具有預設值的參數,則必須使用 @JvmOverloads
。如果沒有這項註解,就無法使用任何預設值叫用函式。
使用 @JvmOverloads
時,請檢查產生的方法,確認各個方法是否合理。如果方法不合理,請執行以下一項或兩項重構,直到滿意為止:
- 變更參數順序,先用預設接近末端的參數。
- 將預設移入手動函式超載。
錯誤:無 @JvmOverloads
class Greeting { fun sayHello(prefix: String = "Mr.", name: String) { println("Hello, $prefix $name") } }
public class JavaClass { public static void main(String... args) { Greeting greeting = new Greeting(); greeting.sayHello("Mr.", "Bob"); } }
正確:@JvmOverloads
註解。
class Greeting { @JvmOverloads fun sayHello(prefix: String = "Mr.", name: String) { println("Hello, $prefix $name") } }
public class JavaClass { public static void main(String... args) { Greeting greeting = new Greeting(); greeting.sayHello("Bob"); } }
Lint 檢查
相關規定
- Android Studio 版本:3.2 初期測試版本 10 或以上
- Android Gradle 外掛程式版本:3.2 或以上
支援的檢查
現在,你可以透過 Android Lint 檢查來偵測和標記上述某些互通性問題。目前只能偵測 Java (搭配 Kotlin) 中的問題。具體來說,支援的檢查如下:
- 未知的空值
- 屬性存取權
- 沒有硬式 Kotlin 關鍵字
- 上次 Lambda 參數
Android Studio
如要啟用這些檢查,請依序前往「File」(檔案) >「Preferences」(偏好設定) >「Editor」(編輯器) >「Inspections」(檢查),然後在「Kotlin Interoperability」(Kotlin 互通性) 下勾選您要啟用的規則:
圖 1. Android Studio 中的 Kotlin 互通性設定。
檢查要啟用的規則後,系統會在您執行程式碼檢查時執行新的檢查 (「Analyze」(分析) >「Inspect Code」(檢查程式碼))。
指令列版本
如要透過指令列版本啟用這些檢查,請在 build.gradle
檔案中新增以下指令列:
Groovy
android { ... lintOptions { enable 'Interoperability' } }
Kotlin
android { ... lintOptions { enable("Interoperability") } }
如要瞭解 lintOptions 內支援的整套設定,請參閱「Android Gradle DSL 參考資料」。
然後,再透過指令列執行 ./gradlew lint
。