Kotlin-Java 相互運用ガイド

このドキュメントは、Java と Kotlin で相互運用可能な公開 API を作成するための一連のルールです。これらのルールは、他の言語からも違和感なくコードを使用できるようにする目的で設定されました。

最終更新日: 2018-05-18

Java(Kotlin で使用する場合)

ハード キーワードなし

Kotlin のハード キーワードをメソッドやフィールドの名前として使用しないでください。Kotlin から呼び出す場合、これらには、エスケープのためのバッククォートを使用する必要があります。ソフト キーワード修飾子キーワード特殊識別子は使用できます。

たとえば、Mockito の when 関数を Kotlin から使用する場合、バッククォートが必要です。

val callable = Mockito.mock(Callable::class.java)
Mockito.`when`(callable.call()).thenReturn(/* … */)

Any 拡張機能名を回避する

絶対に必要な場合を除き、メソッドに Any の拡張関数の名前を使用することや、フィールドに Any の拡張プロパティの名前を使用することは避けてください。メンバーのメソッドとフィールドは常に Any の拡張関数またはプロパティよりも優先されますが、コードを読む際に呼び出し対象を判断するのが難しくなる場合があります。

null 値許容アノテーション

公開 API におけるすべての非プリミティブ パラメータ、戻り値、フィールド型に、null 値許容アノテーションが必要です。非アノテーション型は「プラットフォーム」型として解釈され、null 値許容の可否が不明確になります。

デフォルトでは、Kotlin コンパイラ フラグは JSR 305 アノテーションを受け入れますが、警告が報告されます。コンパイラがアノテーションをエラーとして処理するようにフラグを設定することもできます。

ラムダ パラメータが最後にある

SAM 変換に有効なパラメータ型は最後に置く必要があります。

たとえば、RxJava 2’s Flowable.create() メソッド シグネチャは次のように定義されます。

public static  Flowable create(
    FlowableOnSubscribe source,
    BackpressureStrategy mode) { /* … */ }

FlowableOnSubscribe は SAM 変換に有効であるため、Kotlin からのこのメソッドの関数呼び出しは次のようになります。

Flowable.create({ /* … */ }, BackpressureStrategy.LATEST)

ただし、パラメータがメソッド シグネチャで反転している場合、関数呼び出しでは trailing-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 を追加して複数のファイルのトップレベル メンバーを 1 つのクラスにまとめることを考慮してください。

ラムダ引数

Java で定義されているシングル メソッド インターフェース(SAM)は、ラムダ構文を使用して Kotlin と Java の両方で実装することができ、慣用的な方法で実装をインライン化します。Kotlin にはこのインターフェースを定義するオプションがいくつかありますが、それぞれに若干違いがあります。

推奨される定義

Java から使用するための高階関数では、Java 呼び出し元が Unit.INSTANCE を返す必要があるため、Unit を返す関数型を採用しないようにしてください。シグネチャで関数型をインライン化する代わりに、関数(SAM)インターフェースを使用してください。ラムダとして使用されることが想定されるインターフェースを定義する場合も、通常のインターフェースではなく関数(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 を返さない関数型の場合でも、(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;
});

状態を持つことを意図した実装の場合は機能インターフェースを避ける

状態を持つことを意図したインターフェースの実装の場合、ラムダ構文の使用は意味がありません。典型的な例が Comparable です。Comparable は thisother を比較することを目的としていますが、ラムダには this がありません。インターフェースに fun の接頭辞を付けないことで、呼び出し元は object : ... 構文の使用を強制されます。これにより、状態を持つことが可能になり、呼び出し元にヒントを提供できます。

以下の Kotlin の定義を例にしてみましょう。

// No "fun" prefix.
interface Counter {
  fun increment()
}

これにより Kotlin でラムダ構文が使用されなくなり、以下のような長いバージョンが必要になります。

runCounter(object : Counter {
  private var increments = 0 // State

  override fun increment() {
    increments++
  }
})

Nothing のジェネリクスを回避する

ジェネリック パラメータが Nothing である型は、原型(raw type)として Java に公開されます。原型が Java で使用されるのはまれなので、回避する必要があります。

例外を文書化する

チェック済みの例外をスローできる関数では、@Throws でそれらの例外を文書化する必要があります。ランタイム例外は KDoc で文書化する必要があります。

関数のデリゲート先となる API に留意してください。これらの API からはチェック済みの例外がスローされる場合があるためです。これが行われないと、Kotlin では気付かれないままチェック済みの例外が許可されます。

防御的コピー(Defensive copy)

共有または所有者不在の読み取り専用コレクションを公開 API から返すときは、変更不可能なコンテナにラップするか、防御的コピー(defensive copy)を実行してください。 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();
    }
}

コンパニオン定数

companion object 内の有効定数である、const 以外の公開プロパティを静的フィールドとして公開するためには、@JvmField のアノテーションを付ける必要があります。

アノテーションがない場合、これらのプロパティは、静的 Companion フィールド上の非標準名インスタンス「getters」としてしか使用できなくなります。@JvmField ではなく @JvmStatic を使用すると、非標準的な名前の「getters」は、クラス上の静的メソッドに移されますが、それでもまだ正しくありません。

誤: アノテーションがありません

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 Canary 10 以降
  • Android Gradle プラグイン バージョン: 3.2 以降

サポート対象のチェック

上述した相互運用性の問題の一部を、Android lint チェックで検出および報告できるようになりました。現在検出されるのは、Java(Kotlin で使用される場合)の問題のみです。サポート対象のチェックは次のとおりです。

  • null 値許容性が不明
  • プロパティのアクセス
  • ハードな Kotlin キーワードがない
  • ラムダ パラメータが最後にある

Android Studio

これらのチェックを有効にするには、[File] > [Preferences] > [Editor] > [Inspections] の順に移動し、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 を実行します。