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 中定義的單一方法介面 (SAM) 可以使用 lambda 語法在 Kotlin 和 Java 中實作,並以慣用方式內嵌實作。Kotlin 提供多個定義這類介面的選項,每個選項都有些微差異。

建議使用的定義

專為 Java 使用的高排序函式不應採用傳回 Unit函式類型,因為需要 Java 呼叫端傳回 Unit.INSTANCE。請使用功能 (SAM) 介面,不要在簽名中內嵌函式類型。定義預期將做為 lambda 使用的介面時,請考慮使用功能性 (SAM) 介面,而非一般介面。

請考慮以下 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,仍建議您將其設為具名介面,讓呼叫端能夠使用具名類別,而不是在 Kotlin 和 Java 中以 lambda 進行實作。

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 語法並不合理。「可比較」是一個明顯的範例,因為這會將 thisother 進行比較,且 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
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

如要啟用這些檢查,請依序前往「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