Kotlin-Java 互通性指南

本文件說明一系列在 Java 和 Kotlin 中編寫公用 API 的規則,目的是讓程式碼在以其他語言使用時符合該語言的習慣。

上次更新:2018 年 5 月 18 日

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 static  Flowable 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 Optional
data 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";
    Optional 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

如要啟用這些檢查,請依序前往「檔案」 > 「偏好設定」 > 「編輯器」 > 「檢查」,然後在「Kotlin 互通性」下勾選您要啟用的規則:

圖 1. Android Studio 中的 Kotlin 互通性設定。

檢查要啟用的規則後,系統會在您執行程式碼檢查時執行新的檢查 (「分析」 > 「檢查程式碼」…)。

指令列版本

如要透過指令列版本啟用這些檢查,請在 build.gradle 檔案中新增以下指令列:

Groovy

android {

    ...

    lintOptions {
        enable 'Interoperability'
    }
}

Kotlin

android {
    ...

    lintOptions {
        enable("Interoperability")
    }
}

如要瞭解 lintOptions 內支援的整套設定,請參閱 Android 「Gradle DSL 參考資料」

然後,再透過指令列執行 ./gradlew lint