QUO CARD Digital Innovation Lab Tech Blog

クオカード デジタルイノベーションラボの技術ブログです

Kotlin で Java15 の sealed class をいじってみたけど、 Kotlin は 2020 年 10 月頭の段階で Java15 対応が入ってないので失敗した話

こんにちは、 クオカードデジタルイノベーションラボの Kotlin おじさんこと 持田 です。 Java 15 がリリースされたので、さっそく触ってみました(Kotlin で)。

build.gradle

build.gradle ファイルはこんな感じになります。

Kotlin プラグインの作りの関係上、 Java のプロジェクトと Kotlin のプロジェクトを同じものにすると Javaコンパイル後に Kotlin をコンパイルさせるためのビルドスクリプトを構築するのが面倒なので、言語ごとにサブプロジェクトとしてわけて Kotlin プロジェクトが Java のプロジェクトに依存する形にします。

また、プレビューの機能を利用するので、 --enable-preview オプションをコンパイラー、アプリケーションのパラメーターに付与しています。

プロジェクト java-project

plugins {
  id 'java-library'
}

tasks.withType(JavaCompile) {
  options.compilerArgs.addAll(['--enable-preview'])
}

プロジェクト kotlin-project

plugins {
  id 'org.jetbrains.kotlin.jvm' version '1.4.10'
  id 'application'
}

repositories {
    jcenter()
}

dependencies {
    implementation(project(':java-project'))
    implementation platform('org.jetbrains.kotlin:kotlin-bom')
    implementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk8'
    testImplementation 'org.jetbrains.kotlin:kotlin-test'
    testImplementation 'org.jetbrains.kotlin:kotlin-test-junit5'
}

application {
    mainClassName = 'com.example.AppKt'
    applicationDefaultJvmArgs = ['--enable-preview']
}

tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile) {
    kotlinOptions {
        jvmTarget = "14"
    }
}

なお、 Kotlin のコンパイルオプションの jvmTarget14 以上が指定できないので、とりあえず 14 です。 これはなんだかエラーが発生しそうな匂いがプンプンします!

sealed class

Kotlin でも sealed class がありましたが、 Java にも sealed class が登場です。 Kotlin の sealed class は同一ファイル内限定で継承可能なクラスでしたが、 Java の sealed class(interface) 基底のクラス(インターフェース)が実装できるクラスを限定する形式になっています。 では早速実装を指定するインターフェースを作ってみましょう。

java-project/src/main/java/com/example/Foo.java
public sealed interface Foo permits Bar {}

このままコンパイルをすると java-project プロジェクトの方でコンパイルエラーが発生します。

/path/to/project/java-project/src/main/java/com/example/Foo.java:18: エラー: シンボルを見つけられません
public sealed interface Foo permits Bar {
                                    ^
  シンボル: クラス Bar

それはそうですね…実装を指定しているのですから、そのクラスをコンパイラーが参照できなければ失敗しますね。 ということで、 kotlin-project の方で…というわけにもいきません。 kotlin-project の方は java-project に依存しているわけですので、 java-project の方がコンパイルが通らないとコンパイルできません。 つまり、 Java の sealed クラスは java-project の方でのみ実装しておかないといけません。

というわけで、適当に実装を作っておきます。

java-project/src/main/java/com/example/Bar.java
enum Bar implements Foo {
  QUX
}

なお、ここでは Foosealed interface にしたため、 Barenum にできましたが、 sealed class にした場合は、 二重継承になってしまうので(enum から作られるクラスは Enum<E> クラスを継承する)、 enum を実装にできません。


…適当に実装を作ったものの、 java-project で完結してたら Kotlin 出てくる余地がありませんね…

というのは置いといて、 この sealed class は Java コンパイラーにとっては特別でも 外部の利用者から見れば単なる抽象クラス(インターフェース)でしかないので、 Kotlin でも実装できるはず!!!と期待をこめて(何の?)挑戦してみましょう。

kotlin-project/src/kotlin/com/example/Baz.kt
data class Baz(val item: String): Foo

では、コンパイルしてみようと思います。

Executing task 'kotlin-project:classes'


> Task :java-project:compileJava
ノート:一部の入力ファイルはプレビュー言語機能を使用します。
ノート:詳細は、-Xlint:previewオプションを指定して再コンパイルしてください。

> Task :kotlin-project:compileKotlin
> Task :kotlin-project:compileJava NO-SOURCE
> Task :kotlin-project:processResources NO-SOURCE
> Task :kotlin-project:classes UP-TO-DATE
> Task :java-project:processResources NO-SOURCE
> Task :java-project:classes

BUILD SUCCESSFUL in 395ms

お、コンパイルできましたね…つまり、 Kotlin においては、 Java の sealed は封されてなかったことが示されました。

では、アプリを実行してみましょう!

Executing task 'kotlin-project:run'


> Task :java-project:compileJava
ノート:一部の入力ファイルはプレビュー言語機能を使用します。
ノート:詳細は、-Xlint:previewオプションを指定して再コンパイルしてください。

> Task :kotlin-project:compileKotlin
> Task :kotlin-project:compileJava NO-SOURCE
> Task :kotlin-project:processResources NO-SOURCE
> Task :kotlin-project:classes UP-TO-DATE
> Task :java-project:processResources NO-SOURCE
> Task :java-project:classes
> Task :java-project:jar

Task :kotlin-project:run FAILED

4 actionable tasks: 2 executed, 2 up-to-date
Picked up _JAVA_OPTIONS: -Dfile.encoding=UTF-8
Exception in thread "main" java.lang.IncompatibleClassChangeError: class com.example.Baz cannot implement sealed interface com.example.Foo
    at java.base/java.lang.ClassLoader.defineClass1(Native Method)
    at java.base/java.lang.ClassLoader.defineClass(ClassLoader.java:1016)
    at java.base/java.security.SecureClassLoader.defineClass(SecureClassLoader.java:151)
    at java.base/jdk.internal.loader.BuiltinClassLoader.defineClass(BuiltinClassLoader.java:825)
    at java.base/jdk.internal.loader.BuiltinClassLoader.findClassOnClassPathOrNull(BuiltinClassLoader.java:723)
    at java.base/jdk.internal.loader.BuiltinClassLoader.loadClassOrNull(BuiltinClassLoader.java:646)
    at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:604)
    at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:168)
    at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:522)
    at com.example.AppKt.main(App.kt:11)
    at com.example.AppKt.main(App.kt)

FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':run'.
> Process 'command '/path/to/java/15-open/bin/java'' finished with non-zero exit value 1

* Try:
Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output. Run with --scan to get full insights.

* Get more help at https://help.gradle.org

BUILD FAILED in 169ms

やりました!落ちました!世紀の大発見!我々は Kotlin コンパイラーの未対応箇所を見つけました!


さて、未対応なものについては、レポートで報告しないといけません。 Kotlin の不具合を見つけたら、 YouTrack に報告します。

https://youtrack.jetbrains.com/issues/KT

Duplicate になると嫌だし、 JetBrains の人も嫌がると思うので、検索してなければ登録という手順で登録します。

今回は Java15sealed class といったキーワードがあるので、何度か検索方法を調整しつつ検索すると…

https://youtrack.jetbrains.com/issues/KT?q=%22JDK%2015%22%20%22sealed%20class%22

すでに一ヶ月前に issue が 作られているようでした。

https://youtrack.jetbrains.com/issue/KT-41215

JDK 15+: Recognize PermittedSubclasses attribute during compilation and appropriately forbid extensions

やはり、凡人の我々が思いつくようなことは、世の中の頭の良い人が既にもう試しているものなんですね…

Kotlin に Java15 対応が入ったときに再び試してみたいと思います。


以上、 Java15 の sealed class を試してみました。 Kotlin の JVM オプションに 15 がない時点で想定できた結果でしたが、 実際にやってみると Kotlin または Java を単体で触ってるだけではわからないような、以下のことがわかってきます。

  • Javaコンパイラーによる制約は当然ながら Kotlin コンパイラーには引き継がれてないので、その重箱の隅を突っつくような実装はプロダクションでは避ける
  • --enable-previewコンパイル時と実行時の両方で必要

以下テンプレです! デジタルイノベーションラボでは新しいものやわからないものでも 積極的に実験して自分のものにしてしまう、 そんな若気の至りに毛が生えたようなクロスファンクショナルなエンジニアを募集しています。 われこそはという方は こちら まで 是非是非ご応募してください!!!!