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

最近、Kotlinを本格的に触るようになってきたので、いくつかつまづく事も多々ありました。
そろそろ記事にできたらいいなと思いその中のひとつ、インターフェイスのデフォルトメソッドの相互運用について記載しておきます。

デフォルトメソッドの扱い

kotlinで普通にインターフェイスのデフォルトメソッドを使うと、
コンパイルしたクラスファイルは、interfaceのデフォルトメソッドは、別の形で実現されています。

JavaやGroovyと相互運用する場合には、注意が必要です。

jvmTarget を指定しない場合、kotlinはもともとJava6と互換のあるクラスファイルを出力していたため、Java8で追加されたデフォルトメソッドの形では当時は出力ができなかった。

現在のkotlin(1.5)は、jvmTarget を指定しない場合にはデフォルトでJava8を出力します。
しかし、その頃の互換性を維持するために、今でもデフォルトメソッドは昔のままの形で出力するようです。

これは後述しますが、kotlinのコンパイルオプションを変更することで、出力されるクラスファイルの形式を制御することができます。

kotlinコンパイラのデフォルトの挙動

kotlinのコンパイルオプションを変更することで、デフォルトメソッドの出力形式を変更する事ができますが、
まず、オプション指定しない場合にどのような挙動になるのか整理しておきましょう。

デフォルトメソッドの実装は内部クラスに移動する。

kotlinコンパイラーは、インターフェイスのデフォルトメソッドに対して、どのようなクラスファイルを出力するのか?

具体的には、DefaultImplsという内部クラスが生成されています。

例えば、下記のようなインターフェイスをkotlinで書いた場合。

interface Hoge {
    fun methodA() { println("hello") }  // <- これがデフォルトメソッド
    fun methodB() 
}

kotlinコンパイル後は、元々のinterfaceのデフォルトメソッドは、未実装のメソッドとしてシグネチャは残り、実装自体は内部クラスとして生成されたDefaultImplsクラスに、static メソッドとして定義が移ります。

Javaでいうと下記のようなコードになって出力されてしまいます。


interface Hoge {
    void methodA(); // <- デフォルトメソッドは、未実装に変わる。
    void methodB();
}

interface Hoge {
    void methodA();
    void methodB();

    // DefaultImplsという内部クラスができる。
    public static final class DefaultImpls { 
            // デフォルトメソッドの実装がここに移る。
            public static void methodA() {
                  System.out.println("hello");
          }
    }
}

デフォルトメソッドだったmethodAは、未実装(abstract)に変わってしまいます。

そのため、JavaやGroovyで、このHogeインターフェイスをimplements する際には、methodAをOverrideして実装する必要がでてきます。

もし、Overrideができていないと、このようなビルドエラーが発生します。

Can't have an abstract method in a non-abstract class.
The class '...' must be declared abstract or the method '...' must be implemented.

Kotlinで書いたインターフェイスをJava/Groovyで、implements する場合は、デフォルトメソッドが abstractメソッドに変わっているので注意しましょう。

kotlinコンパイルオプションを変更する。

今まで述べた挙動は、kotlinのコンパイルオプションを変更することで、
Java8以降のインターフェイスのデフォルトメソッドとして、ちゃんとクラスファイル出力されるように設定する事が可能です。

ここでは、Gradleを使ったkotlinコンパイルオプションの設定方法を紹介します。

kotoin1.5では、jvmTargetはデフォルトで1.8(Java8)です。

インターフェイスのデフォルトメソッドが使えるようになったのは、Java8以降なので、
デフォルトメソッドを有効にする場合、jvmTargetは特に指定する必要はありません。(1.6以外、1.8以降であればOK)

デフォルトメソッドに関する挙動を指定するコンパイルオプションは、
-XJvm-default=all です。

もし、デフォルトメソッドとして出力すると同時に、
今までの挙動と互換性のある内部クラス(DefaultImpls)も出力してほしいという場合は、
-Xjvm-default=all-compatibilityを指定する事ができます。

これらの指定をすることで、kotlinコンパイラは
デフォルトメソッドの形でクラスファイルを正しく出力してくれます。

build.gradke.kts の指定方法

import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

// kotlinコンパイルのすべてのタスクに対して設定。
tasks.withType(KotlinCompile::class).all {
    kotlinOptions {
        // Java8以降であればOK
        //(kotlin1.5以降では、デフォルトで1.8)
        jvmTarget = "16"

        // インターフェイスのデフォルトメソッドを利用
        freeCompilerArgs = listOf("-Xjvm-default=all")
    }
}

この状態であれば、kotlinで書いたインターフェイスを、
JavaやGroovyから使う場合であっても、特に気にせずに扱う事ができます。

kotlin1.4で使われていた下記のアノテーションと、kotlinコンパイルオプションはdeprecatedになっています。

  • @JvmDefault
  • -Xjvm-default=enable
  • -Xjvm-default=compatibility

まとめ

  • kotlin1.5以降はデフォルトでjvmTargetはJava8になっている。
  • インターフェイスのデフォルトメソッドは、何も指定しない場合、DefaultImplsという内部クラスとなる。
  • Java/Groovyで相互運用でimplementsする場合には、注意しないといけない。
  • -XJvm-default=all をkotlinコンパイルに指定することで、本来のデフォルトメソッドの形で出力してくれる。

コメント

タイトルとURLをコピーしました