Skip to content

JavaからKotlinを呼び出す

KotlinコードはJavaから簡単に呼び出すことができます。 例えば、KotlinクラスのインスタンスはJavaメソッド内でシームレスに作成および操作できます。 しかし、KotlinコードをJavaに統合する際には、JavaとKotlinの間に注意すべきいくつかの違いがあります。 このページでは、KotlinコードとJavaクライアントとの相互運用性(interop)を調整する方法について説明します。

プロパティ

Kotlinのプロパティは、以下のJava要素にコンパイルされます。

  • ゲッターメソッド(getプレフィックスを前に付けて名前が計算されます)
  • セッターメソッド(setプレフィックスを前に付けて名前が計算されます)(varプロパティのみ)
  • プライベートフィールド(プロパティ名と同じ名前)(バッキングフィールドを持つプロパティのみ)

例えば、var firstName: String は以下のJava宣言にコンパイルされます。

java
private String firstName;

public String getFirstName() {
    return firstName;
}

public void setFirstName(String firstName) {
    this.firstName = firstName;
}

プロパティ名がisで始まる場合、異なる名前マッピングルールが使用されます。ゲッターの名前はプロパティ名と同じになり、セッターの名前はissetに置き換えることで得られます。 例えば、プロパティisOpenの場合、ゲッターはisOpen()、セッターはsetOpen()と呼び出されます。 このルールは、Booleanだけでなく、あらゆる型のプロパティに適用されます。

パッケージレベル関数

org.exampleパッケージ内のapp.ktファイルで宣言されたすべての関数とプロパティ(拡張関数を含む)は、org.example.AppKtという名前のJavaクラスの静的メソッドにコンパイルされます。

kotlin
// app.kt
package org.example

class Util

fun getTime() { /*...*/ }
java
// Java
new org.example.Util();
org.example.AppKt.getTime();

生成されるJavaクラスにカスタム名を指定するには、@JvmNameアノテーションを使用します。

kotlin
@file:JvmName("DemoUtils")

package org.example

class Util

fun getTime() { /*...*/ }
java
// Java
new org.example.Util();
org.example.DemoUtils.getTime();

生成されるJavaクラス名が同じ(同じパッケージとファイル名、または同じ@JvmNameアノテーション)の複数のファイルを持つことは、通常はエラーです。 しかし、コンパイラは、指定された名前を持ち、その名前を持つすべてのファイルからの宣言をすべて含む単一のJavaファサードクラスを生成できます。 そのようなファサードの生成を有効にするには、すべての該当ファイルで@JvmMultifileClassアノテーションを使用します。

kotlin
// oldutils.kt
@file:JvmName("Utils")
@file:JvmMultifileClass

package org.example

fun getTime() { /*...*/ }
kotlin
// newutils.kt
@file:JvmName("Utils")
@file:JvmMultifileClass

package org.example

fun getDate() { /*...*/ }
java
// Java
org.example.Utils.getTime();
org.example.Utils.getDate();

インスタンスフィールド

KotlinプロパティをJavaのフィールドとして公開する必要がある場合は、@JvmFieldアノテーションを付けます。 フィールドは、基となるプロパティと同じ可視性(visibility)を持ちます。@JvmFieldアノテーションを付けることができるプロパティは以下の通りです。

  • バッキングフィールドを持つ
  • privateでない
  • openoverrideconst修飾子を持たない
  • 委譲プロパティでない
kotlin
class User(id: String) {
    @JvmField val ID = id
}
java

// Java
class JavaClient {
    public String getID(User user) {
        return user.ID;
    }
}

遅延初期化プロパティもフィールドとして公開されます。 フィールドの可視性は、lateinitプロパティのセッターの可視性と同じになります。

静的フィールド

名前付きオブジェクトまたはコンパニオンオブジェクトで宣言されたKotlinプロパティは、その名前付きオブジェクト内、またはコンパニオンオブジェクトを含むクラス内に静的バッキングフィールドを持ちます。

通常、これらのフィールドはprivateですが、以下のいずれかの方法で公開できます。

  • @JvmFieldアノテーション
  • lateinit修飾子
  • const修飾子

そのようなプロパティに@JvmFieldアノテーションを付けると、プロパティ自体と同じ可視性を持つ静的フィールドになります。

kotlin
class Key(val value: Int) {
    companion object {
        @JvmField
        val COMPARATOR: Comparator<Key> = compareBy<Key> { it.value }
    }
}
java
// Java
Key.COMPARATOR.compare(key1, key2);
// public static final field in Key class

オブジェクトまたはコンパニオンオブジェクト内の遅延初期化プロパティは、プロパティセッターと同じ可視性を持つ静的バッキングフィールドを持ちます。

kotlin
object Singleton {
    lateinit var provider: Provider
}
java

// Java
Singleton.provider = new Provider();
// public static non-final field in Singleton class

constとして宣言されたプロパティ(クラス内およびトップレベルの両方)は、Javaでは静的フィールドに変換されます。

kotlin
// file example.kt

object Obj {
    const val CONST = 1
}

class C {
    companion object {
        const val VERSION = 9
    }
}

const val MAX = 239

Javaでは:

java

int constant = Obj.CONST;
int max = ExampleKt.MAX;
int version = C.VERSION;

静的メソッド

上述の通り、Kotlinはパッケージレベルの関数を静的メソッドとして表現します。 名前付きオブジェクトまたはコンパニオンオブジェクトで定義された関数を@JvmStaticとしてアノテーションした場合、Kotlinはそれらの関数に対して静的メソッドも生成できます。 このアノテーションを使用すると、コンパイラはオブジェクトの囲むクラス内の静的メソッドと、オブジェクト自体のインスタンスメソッドの両方を生成します。例えば:

kotlin
class C {
    companion object {
        @JvmStatic fun callStatic() {}
        fun callNonStatic() {}
    }
}

これにより、callStatic()はJavaで静的になりますが、callNonStatic()は静的ではありません。

java

C.callStatic(); // works fine
C.callNonStatic(); // error: not a static method
C.Companion.callStatic(); // instance method remains
C.Companion.callNonStatic(); // the only way it works

名前付きオブジェクトでも同様です。

kotlin
object Obj {
    @JvmStatic fun callStatic() {}
    fun callNonStatic() {}
}

Javaでは:

java

Obj.callStatic(); // works fine
Obj.callNonStatic(); // error
Obj.INSTANCE.callNonStatic(); // works, a call through the singleton instance
Obj.INSTANCE.callStatic(); // works too

Kotlin 1.3以降、@JvmStaticはインターフェースのコンパニオンオブジェクトで定義された関数にも適用されます。 そのような関数はインターフェース内の静的メソッドにコンパイルされます。インターフェースの静的メソッドはJava 1.8で導入されたため、対応するターゲットを使用するように注意してください。

kotlin
interface ChatBot {
    companion object {
        @JvmStatic fun greet(username: String) {
            println("Hello, $username")
        }
    }
}

@JvmStaticアノテーションは、オブジェクトまたはコンパニオンオブジェクトのプロパティにも適用でき、そのゲッターメソッドとセッターメソッドを、そのオブジェクトまたはコンパニオンオブジェクトを含むクラスの静的メンバーにすることができます。

インターフェースのデフォルトメソッド

NOTE

デフォルトメソッドはJVM 1.8以降のターゲットでのみ利用可能です。

JDK 1.8以降、Javaのインターフェースはデフォルトメソッドを含むことができます。 Kotlinインターフェースのすべての非抽象メンバーを、それを実装するJavaクラスのデフォルトにするには、Kotlinコードを-Xjvm-default=allコンパイラオプションでコンパイルします。

デフォルトメソッドを持つKotlinインターフェースの例です。

kotlin
// compile with -Xjvm-default=all

interface Robot {
    fun move() { println("~walking~") }  // Javaインターフェースではデフォルトになります
    fun speak(): Unit
}

デフォルトの実装は、そのインターフェースを実装するJavaクラスで利用可能です。

java
//Java implementation
public class C3PO implements Robot {
    // move() implementation from Robot is available implicitly
    @Override
    public void speak() {
        System.out.println("I beg your pardon, sir");
    }
}
java
C3PO c3po = new C3PO();
c3po.move(); // default implementation from the Robot interface
c3po.speak();

インターフェースの実装は、デフォルトメソッドをオーバーライドできます。

java
//Java
public class BB8 implements Robot {
    //デフォルトメソッドの独自の実装
    @Override
    public void move() {
        System.out.println("~rolling~");
    }

    @Override
    public void speak() {
        System.out.println("Beep-beep");
    }
}

NOTE

Kotlin 1.4より前では、デフォルトメソッドを生成するために、これらのメソッドに@JvmDefaultアノテーションを使用できました。

1.4以降で-Xjvm-default=allでコンパイルすると、通常はインターフェースのすべての非抽象メソッドに@JvmDefaultをアノテーションし、-Xjvm-default=enableでコンパイルしたかのように動作します。ただし、動作が異なる場合があります。

Kotlin 1.4でのデフォルトメソッド生成の変更に関する詳細情報は、Kotlinブログのこちらの記事に記載されています。

デフォルトメソッドの互換性モード

-Xjvm-default=allオプションなしでコンパイルされたKotlinインターフェースを使用しているクライアントがいる場合、このオプションでコンパイルされたコードとはバイナリ互換性がない可能性があります。そのようなクライアントとの互換性を損なわないようにするには、-Xjvm-default=allモードを使用し、インターフェースに@JvmDefaultWithCompatibilityアノテーションを付けます。 これにより、一度公開APIのすべてのインターフェースにこのアノテーションを追加すれば、新しい非公開コードに対してはアノテーションを使用する必要がなくなります。

NOTE

Kotlin 1.6.20以降では、デフォルトモード(-Xjvm-default=disableコンパイラオプション)のモジュールを、-Xjvm-default=allまたは-Xjvm-default=all-compatibilityモードでコンパイルされたモジュールに対してコンパイルできます。

互換性モードの詳細:

disable

デフォルトの動作です。JVMデフォルトメソッドを生成せず、@JvmDefaultアノテーションの使用を禁止します。

all

モジュール内のボディを持つすべてのインターフェース宣言に対してJVMデフォルトメソッドを生成します。disableモードでデフォルトで生成される、ボディを持つインターフェース宣言に対するDefaultImplsスタブは生成しません。

インターフェースがdisableモードでコンパイルされたインターフェースからボディを持つメソッドを継承し、それをオーバーライドしない場合、そのメソッドに対してDefaultImplsスタブが生成されます。

DefaultImplsクラスの存在にクライアントコードが依存している場合、バイナリ互換性が損なわれます

NOTE

インターフェースの委譲が使用されている場合、すべてのインターフェースメソッドが委譲されます。唯一の例外は、非推奨の@JvmDefaultアノテーションが付けられたメソッドです。

all-compatibility

allモードに加えて、DefaultImplsクラス内に互換性スタブを生成します。互換性スタブは、ライブラリおよびランタイムの作成者にとって、以前のライブラリバージョンに対してコンパイルされた既存のクライアントとの後方バイナリ互換性を維持するために役立ちます。 allおよびall-compatibilityモードは、ライブラリの再コンパイル後にクライアントが使用するライブラリのABI(Application Binary Interface)表面を変更します。 その意味で、クライアントは以前のライブラリバージョンと互換性がなくなる可能性があります。 これは通常、適切なライブラリのバージョン管理、例えばSemVerにおけるメジャーバージョンアップが必要であることを意味します。

コンパイラは、DefaultImplsのすべてのメンバーを@Deprecatedアノテーション付きで生成します。これらのメンバーは互換性の目的でのみ生成されるため、Javaコードで使用すべきではありません。

allまたはall-compatibilityモードでコンパイルされたKotlinインターフェースからの継承の場合、DefaultImpls互換性スタブは、標準のJVMランタイム解決セマンティクスでインターフェースのデフォルトメソッドを呼び出します。

disableモードで特殊化されたシグネチャを持つ追加の暗黙的メソッドが生成される場合がある、ジェネリックインターフェースを継承するクラスに対して、追加の互換性チェックを実行します。 disableモードとは異なり、そのようなメソッドを明示的にオーバーライドせず、クラスに@JvmDefaultWithoutCompatibilityアノテーションを付けない場合、コンパイラはエラーを報告します(詳細はこのYouTrackイシューを参照してください)。

可視性

Kotlinの可視性修飾子は、Javaに以下のようにマッピングされます。

  • privateメンバーはprivateメンバーにコンパイルされます。
  • privateトップレベル宣言はprivateトップレベル宣言にコンパイルされます。クラス内からアクセスされる場合は、パッケージプライベートなアクセサーも含まれます。
  • protectedprotectedのままです(Javaは同じパッケージ内の他のクラスからprotectedメンバーにアクセスできますが、Kotlinはできません。そのため、Javaクラスはコードに対してより広いアクセス権を持つことになります)。
  • internal宣言はJavaではpublicになります。internalクラスのメンバーは名前マングリングが行われ、Javaから誤って使用することを難しくし、Kotlinのルールに従ってお互いを認識しない同じシグネチャを持つメンバーのオーバーロードを可能にします。
  • publicpublicのままです。

KClass

KotlinメソッドをKClass型の引数で呼び出す必要がある場合があります。 ClassからKClassへの自動変換はないため、Class<T>.kotlin拡張プロパティに相当するものを呼び出すことで手動で行う必要があります。

kotlin
kotlin.jvm.JvmClassMappingKt.getKotlinClass(MainView.class)

@JvmNameによるシグネチャの競合の回避

Kotlinには名前付き関数がありますが、バイトコードでは異なるJVM名が必要となる場合があります。 最も顕著な例は、*型消去(type erasure)*によって発生します。

kotlin
fun List<String>.filterValid(): List<String>
fun List<Int>.filterValid(): List<Int>

これらの2つの関数は、JVMシグネチャがfilterValid(Ljava/util/List;)Ljava/util/List;と同じであるため、並行して定義することはできません。 Kotlinでそれらを同じ名前にしたい場合は、一方(または両方)に@JvmNameアノテーションを付けて、引数として異なる名前を指定できます。

kotlin
fun List<String>.filterValid(): List<String>

@JvmName("filterValidInt")
fun List<Int>.filterValid(): List<Int>

Kotlinからは同じ名前filterValidでアクセスできますが、JavaからはfilterValidfilterValidIntになります。

プロパティxを関数getX()と並行して持つ必要がある場合も、同じテクニックが適用されます。

kotlin
val x: Int
    @JvmName("getX_prop")
    get() = 15

fun getX() = 10

明示的に実装されたゲッターとセッターを持たないプロパティの生成されたアクセサーメソッドの名前を変更するには、@get:JvmName@set:JvmNameを使用できます。

kotlin
@get:JvmName("x")
@set:JvmName("changeX")
var x: Int = 23

オーバーロードの生成

通常、デフォルトの引数を持つKotlin関数を記述した場合、Javaからはすべての引数が存在する完全なシグネチャとしてのみ見えます。 Java呼び出し元に複数のオーバーロードを公開したい場合は、@JvmOverloadsアノテーションを使用できます。

このアノテーションは、コンストラクタ、静的メソッドなどにも機能します。抽象メソッド、インターフェースで定義されたメソッドを含む、抽象メソッドには使用できません。

kotlin
class Circle @JvmOverloads constructor(centerX: Int, centerY: Int, radius: Double = 1.0) {
    @JvmOverloads fun draw(label: String, lineWidth: Int = 1, color: String = "red") { /*...*/ }
}

デフォルト値を持つすべての引数について、これにより、その引数と、引数リスト内でその右にあるすべての引数が削除された、追加のオーバーロードが1つ生成されます。この例では、以下が生成されます。

java
// コンストラクタ:
Circle(int centerX, int centerY, double radius)
Circle(int centerX, int centerY)

// メソッド
void draw(String label, int lineWidth, String color) { }
void draw(String label, int lineWidth) { }
void draw(String label) { }

セカンダリコンストラクタで説明されているように、クラスがすべてのコンストラクタ引数にデフォルト値を持っている場合、引数なしのpublicコンストラクタが生成されることに注意してください。これは、@JvmOverloadsアノテーションが指定されていなくても機能します。

チェック済み例外

Kotlinにはチェック済み例外(checked exceptions)はありません。 そのため、通常、Kotlin関数のJavaシグネチャはスローされる例外を宣言しません。 したがって、Kotlinで次のような関数がある場合:

kotlin
// example.kt
package demo

fun writeToFile() {
    /*...*/
    throw IOException()
}

そして、Javaから呼び出して例外をキャッチしたい場合:

java

// Java
try {
    demo.Example.writeToFile();
} catch (IOException e) { 
    // エラー: writeToFile() はスローリストに IOException を宣言していません
    // ...
}

writeToFile()IOExceptionを宣言していないため、Javaコンパイラからエラーメッセージが表示されます。 この問題を回避するには、Kotlinで@Throwsアノテーションを使用します。

kotlin
@Throws(IOException::class)
fun writeToFile() {
    /*...*/
    throw IOException()
}

Null安全性

JavaからKotlin関数を呼び出す際、非null許容パラメータとしてnullを渡すことを誰も妨げません。 そのため、Kotlinは非nullを期待するすべてのpublic関数に対してランタイムチェックを生成します。 このようにして、Javaコードで即座にNullPointerExceptionが発生します。

共変なジェネリクス

Kotlinクラスが宣言サイト共変性を利用する場合、その使用方法がJavaコードからどのように見えるかには2つの選択肢があります。例えば、次のようなクラスと、それを使用する2つの関数があるとします。

kotlin
class Box<out T>(val value: T)

interface Base
class Derived : Base

fun boxDerived(value: Derived): Box<Derived> = Box(value)
fun unboxBase(box: Box<Base>): Base = box.value

これらの関数をJavaに変換する素朴な方法は次のようになります。

java
Box<Derived> boxDerived(Derived value) { ... }
Base unboxBase(Box<Base> box) { ... }

問題は、KotlinではunboxBase(boxDerived(Derived()))と記述できるのに対し、JavaではBoxクラスがそのパラメータTに対して不変であり、Box<Derived>Box<Base>のサブタイプではないため、それが不可能であることです。 Javaでこれを機能させるには、unboxBaseを次のように定義する必要があります。

java
Base unboxBase(Box<? extends Base> box) { ... }

この宣言は、Javaが持つのは使用サイト共変性のみであるため、Javaのワイルドカード型? extends Base)を使用して、宣言サイト共変性を使用サイト共変性によってエミュレートしています。

Kotlin APIをJavaで動作させるために、コンパイラは、共変に定義されたBoxの場合(または反変に定義されたFooの場合にFoo<? super Bar>)、Box<Super>パラメータとして出現するときにBox<? extends Super>として生成します。 戻り値の場合、ワイルドカードは生成されません。これは、ワイルドカードが生成されるとJavaクライアントがそれらを処理する必要があるため(そして、それは一般的なJavaのコーディングスタイルに反するため)です。 したがって、例の関数は実際に次のように変換されます。

java

// 戻り値の型 - ワイルドカードなし
Box<Derived> boxDerived(Derived value) { ... }
 
// パラメータ - ワイルドカードあり
Base unboxBase(Box<? extends Base> box) { ... }

NOTE

引数の型がfinalの場合、ワイルドカードを生成する意味は通常ありません。そのため、Box<String>はどのような位置にあっても常にBox<String>になります。

デフォルトではワイルドカードが生成されない場所でワイルドカードが必要な場合は、@JvmWildcardアノテーションを使用します。

kotlin
fun boxDerived(value: Derived): Box<@JvmWildcard Derived> = Box(value)
// に変換されます
// Box<? extends Derived> boxDerived(Derived value) { ... }

逆に、ワイルドカードが生成される場所でワイルドカードが不要な場合は、@JvmSuppressWildcardsを使用します。

kotlin
fun unboxBase(box: Box<@JvmSuppressWildcards Base>): Base = box.value
// に変換されます
// Base unboxBase(Box<Base> box) { ... }

NOTE

@JvmSuppressWildcardsは、個々の型引数だけでなく、関数やクラスといった宣言全体にも使用でき、その中のすべてのワイルドカードを抑制できます。

Nothing型の変換

Nothingは、Javaに自然な対応物がないため、特殊です。実際、java.lang.Voidを含むすべてのJava参照型はnullを値として受け入れますが、Nothingはそれすら受け入れません。 そのため、この型をJavaの世界で正確に表現することはできません。 これが、Nothing型の引数が使用される場合にKotlinがraw型を生成する理由です。

kotlin
fun emptyList(): List<Nothing> = listOf()
// に変換されます
// List emptyList() { ... }