本文件說明一系列在 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 的 Flowable.create() 方法簽名定義如下:
public static <T> Flowable<T> create(
FlowableOnSubscribe<T> 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 引數
不論在 Kotlin 還是 Java 中,都可以使用 lambda 語法,實作透過 Java 定義的單一方法介面 (SAM),這種語法會以符合語言習慣的方式內嵌實作項目。Kotlin 提供多個定義這類介面的選項,每個選項略有不同。
建議定義
若是要透過 Java 使用的高階函式,就不應採用會傳回 Unit 的函式類型,因為這類函式會要求 Java 呼叫端傳回 Unit.INSTANCE。請使用功能性 (SAM) 介面,不要在簽章中內嵌函式類型。此外,如要定義會用做 lambda 的介面,建議您使用功能性 (SAM) 介面 (而非一般介面),即可以慣用方式使用 Kotlin。
請參考以下 Kotlin 定義:
fun interface GreeterCallback {
fun greetName(String name)
}
fun sayHi(greeter: GreeterCallback) = /* … */
從 Kotlin 叫用時:
sayHi { println("Hello, $it!") }
從 Java 叫用時:
sayHi(name -> System.out.println("Hello, " + name + "!"));
即使函式類型不會傳回 Unit,仍建議您將其設為具名介面,方便呼叫端使用具名類別實作項目,而非只使用 lambda (在 Kotlin 和 Java 中)。
class MyGreeterCallback : GreeterCallback {
override fun greetName(name: String) {
println("Hello, $name!");
}
}
避免使用會傳回 Unit 的函式類型
請參考以下 Kotlin 定義:
fun sayHi(greeter: (String) -> Unit) = /* … */
這需要 Java 呼叫端傳回 Unit.INSTANCE:
sayHi(name -> {
System.out.println("Hello, " + name + "!");
return Unit.INSTANCE;
});
如果實作項目必須要有狀態,請避免使用功能性介面
如果介面實作項目必須要有狀態,就不適合使用 lambda 語法。Comparable 就是明顯不適合的示例,因為這個介面是用來比較 this 與 other,而 lambda 沒有 this。如果不為介面加上前置字串 fun,系統會強制呼叫端使用 object : ... 語法,使介面能夠擁有狀態,並向呼叫端提供提示。
請參考以下 Kotlin 定義:
// No "fun" prefix.
interface Counter {
fun increment()
}
此定義可禁止在 Kotlin 中使用 lambda 語法,但程式碼較長:
runCounter(object : Counter {
private var increments = 0 // State
override fun increment() {
increments++
}
})
避免使用 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 Optional<T : Any>
data class Some<T : Any>(val value: T): Optional<T>()
object None : Optional<Nothing>()
@JvmName("ofNullable")
fun <T> 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";
Optional<String> optionalString =
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。