Skip to content

K2コンパイラ移行ガイド

Kotlin言語とエコシステムが進化し続けるにつれ、Kotlinコンパイラも同様に進化してきました。その最初のステップは、ロジックを共有し、異なるプラットフォームのターゲットに対するコード生成を簡素化する、新しいJVMおよびJS IR(中間表現)バックエンドの導入でした。そして現在、進化の次のステージとして、K2として知られる新しいフロントエンドが登場しました。

Kotlin K2コンパイラのアーキテクチャ

K2コンパイラの登場により、Kotlinのフロントエンドは完全に書き直され、新しく、より効率的なアーキテクチャを備えるようになりました。新しいコンパイラがもたらす根本的な変更は、より多くのセマンティック情報を含む、1つの統合されたデータ構造を使用することです。このフロントエンドは、意味解析、呼び出し解決(Call resolution)、型推論(Type inference)を実行する役割を担っています。

新しいアーキテクチャと強化されたデータ構造により、K2コンパイラは以下のメリットを提供します。

  • 呼び出し解決と型推論の改善:コンパイラの動作がより一貫したものになり、コードをより深く理解できるようになります。
  • 新しい言語機能のためのシンタックスシュガーの導入が容易に:将来的に新しい機能が導入される際、より簡潔で読みやすいコードを使用できるようになります。
  • コンパイル時間の高速化:コンパイル時間が大幅に短縮される可能性があります。
  • IDEパフォーマンスの強化:IntelliJ IDEAとAndroid StudioはKotlinコードの解析にK2コンパイラを使用し、安定性の向上とパフォーマンスの改善を提供します。詳細については、IDEでのサポートを参照してください。

このガイドでは、以下の内容について説明します。

  • 新しいK2コンパイラの利点。
  • 移行中に遭遇する可能性のある変更点と、それに応じてコードを適応させる方法。
  • 以前のバージョンにロールバックする方法。

新しいK2コンパイラは、2.0.0からデフォルトで有効になっています。Kotlin 2.0.0で提供される新機能および新しいK2コンパイラの詳細については、Kotlin 2.0.0の新機能を参照してください。

パフォーマンスの向上

K2コンパイラのパフォーマンスを評価するために、2つのオープンソースプロジェクト Anki-AndroidExposed でパフォーマンス・テストを実施しました。その結果、以下のような主要なパフォーマンス向上が確認されました。

  • K2コンパイラは、コンパイル速度を最大94%向上させます。例えば、Anki-Androidプロジェクトでは、クリーンビルドの時間がKotlin 1.9.23の57.7秒から、Kotlin 2.0.0では29.7秒に短縮されました。
  • 初期化フェーズは、K2コンパイラによって最大488%高速化されます。例えば、Anki-Androidプロジェクトでは、増分ビルドの初期化フェーズがKotlin 1.9.23の0.126秒から、Kotlin 2.0.0ではわずか0.022秒に短縮されました。
  • Kotlin K2コンパイラは、以前のコンパイラと比較して、解析フェーズが最大376%高速です。例えば、Anki-Androidプロジェクトでは、増分ビルドの解析時間がKotlin 1.9.23の0.581秒から、Kotlin 2.0.0ではわずか0.122秒に短縮されました。

これらの改善の詳細や、K2コンパイラのパフォーマンスをどのように分析したかについては、ブログ記事を参照してください。

言語機能の改善

Kotlin K2コンパイラは、スマートキャストおよびKotlinマルチプラットフォームに関連する言語機能を改善しています。

スマートキャスト

Kotlinコンパイラは、特定の場合にオブジェクトを型に自動的にキャストできるため、明示的に指定する手間を省くことができます。これはスマートキャストと呼ばれます。Kotlin K2コンパイラは、以前よりもさらに多くのシナリオでスマートキャストを実行できるようになりました。

Kotlin 2.0.0では、以下の領域でスマートキャストに関する改善を行いました。

ローカル変数と以降のスコープ

以前は、変数が if 条件内で null でないと評価された場合、その変数はスマートキャストされていました。この変数に関する情報は、if ブロックのスコープ内でさらに共有されていました。

しかし、if 条件ので変数を宣言した場合、if 条件内ではその変数に関する情報が利用できず、スマートキャストを行うことができませんでした。この動作は、when 式や while ループでも見られました。

Kotlin 2.0.0からは、ifwhen、または while 条件で使用する前に変数を宣言した場合、コンパイラによって収集されたその変数に関する情報は、対応するブロック内でスマートキャストのためにアクセス可能になります。

これは、ブール条件を変数に抽出したい場合などに便利です。変数に意味のある名前を付けることで、コードの可読性が向上し、後でその変数を再利用できるようになります。例:

kotlin
class Cat {
    fun purr() {
        println("Purr purr")
    }
}

fun petAnimal(animal: Any) {
    val isCat = animal is Cat
    if (isCat) {
        // Kotlin 2.0.0では、コンパイラはisCatに関する情報に
        // アクセスできるため、animalがCat型にスマートキャストされた
        // ことを認識します。
        // したがって、purr()関数を呼び出すことができます。
        // Kotlin 1.9.20では、コンパイラはスマートキャストを
        // 認識しないため、purr()関数の呼び出しはエラーになります。
        animal.purr()
    }
}

fun main(){
    val kitty = Cat()
    petAnimal(kitty)
    // Purr purr
}

論理or演算子を使用した型チェック

Kotlin 2.0.0では、オブジェクトの型チェックを or 演算子(||)で組み合わせた場合、それらの最も近い共通のスーパータイプ(Closest common supertype)にスマートキャストされます。この変更以前は、常に Any 型にスマートキャストされていました。

そのため、以前はプロパティにアクセスしたり関数を呼び出したりする前に、その後で手動でオブジェクトの型をチェックする必要がありました。例:

kotlin
interface Status {
    fun signal() {}
}

interface Ok : Status
interface Postponed : Status
interface Declined : Status

fun signalCheck(signalStatus: Any) {
    if (signalStatus is Postponed || signalStatus is Declined) {
        // signalStatusは共通のスーパータイプであるStatusにスマートキャストされます
        signalStatus.signal()
        // Kotlin 2.0.0より前では、signalStatusはAny型に
        // スマートキャストされるため、signal()関数の呼び出しは
        // Unresolved referenceエラーを引き起こしていました。
        // signal()関数は、別の型チェックの後にのみ正常に呼び出すことができました:
        
        // check(signalStatus is Status)
        // signalStatus.signal()
    }
}

共通のスーパータイプは、ユニオン型(Union type)近似です。ユニオン型は現在Kotlinではサポートされていません

インライン関数

Kotlin 2.0.0では、K2コンパイラはインライン関数を異なった方法で処理し、他のコンパイラ分析と組み合わせて、スマートキャストが安全かどうかを判断できるようになりました。

具体的には、インライン関数は暗黙的な callsInPlace コントラクトを持つものとして扱われるようになりました。これは、インライン関数に渡されるラムダ関数がその場で呼び出されることを意味します。ラムダ関数がその場で呼び出されるため、コンパイラはラムダ関数がその関数本体内に含まれる変数の参照をリークさせないことを認識できます。

コンパイラはこの知識を他のコンパイラ分析と共に使用して、キャプチャされた変数のいずれかをスマートキャストするのが安全かどうかを決定します。例:

kotlin
interface Processor {
    fun process()
}

inline fun inlineAction(f: () -> Unit) = f()

fun nextProcessor(): Processor? = null

fun runProcessor(): Processor? {
    var processor: Processor? = null
    inlineAction {
        // Kotlin 2.0.0では、コンパイラはprocessorがローカル変数であり、
        // inlineAction()がインライン関数であることを認識しているため、
        // processorへの参照がリークすることはありません。
        // したがって、processorをスマートキャストしても安全です。
      
        // processorがnullでない場合、processorはスマートキャストされます
        if (processor != null) {
            // コンパイラはprocessorがnullでないことを認識しているため、
            // セーフコールは不要です
            processor.process()

            // Kotlin 1.9.20では、セーフコールを実行する必要があります:
            // processor?.process()
        }

        processor = nextProcessor()
    }

    return processor
}

関数型を持つプロパティ

以前のバージョンのKotlinには、関数型を持つクラスプロパティがスマートキャストされないというバグがありました。Kotlin 2.0.0とK2コンパイラではこの動作を修正しました。例:

kotlin
class Holder(val provider: (() -> Unit)?) {
    fun process() {
        // Kotlin 2.0.0では、providerがnullでない場合、
        // スマートキャストされます
        if (provider != null) {
            // コンパイラはproviderがnullでないことを認識しています
            provider()

            // 1.9.20では、コンパイラはproviderがnullでないことを
            // 認識しないため、エラーが発生します:
            // Reference has a nullable type '(() -> Unit)?', use explicit '?.invoke()' to make a function-like call instead
        }
    }
}

この変更は、invoke 演算子をオーバーロードした場合にも適用されます。例:

kotlin
interface Provider {
    operator fun invoke()
}

interface Processor : () -> String

class Holder(val provider: Provider?, val processor: Processor?) {
    fun process() {
        if (provider != null) {
            provider() 
            // 1.9.20では、コンパイラはエラーを発生させます: 
            // Reference has a nullable type 'Provider?', use explicit '?.invoke()' to make a function-like call instead
        }
    }
}

例外処理

Kotlin 2.0.0では、スマートキャスト情報を catch および finally ブロックに渡すことができるよう例外処理を改善しました。この変更により、コンパイラがオブジェクトが null 許容型であるかどうかを追跡するため、コードがより安全になります。例:

kotlin
fun testString() {
    var stringInput: String? = null
    // stringInputはString型にスマートキャストされます
    stringInput = ""
    try {
        // コンパイラはstringInputがnullでないことを認識しています
        println(stringInput.length)
        // 0

        // コンパイラはstringInputの以前のスマートキャスト情報を破棄します。
        // ここでstringInputはString?型になります。
        stringInput = null

        // 例外を発生させる
        if (2 > 1) throw Exception()
        stringInput = ""
    } catch (exception: Exception) {
        // Kotlin 2.0.0では、コンパイラはstringInputが
        // nullになり得ることを認識しているため、stringInputはnull許容のままです。
        println(stringInput?.length)
        // null

        // Kotlin 1.9.20では、コンパイラはセーフコールは不要であると
        // 言いますが、これは誤りです。
    }
}
fun main() {
    testString()
}

インクリメントおよびデクリメント演算子

Kotlin 2.0.0より前、コンパイラはインクリメントまたはデクリメント演算子を使用した後にオブジェクトの型が変わる可能性があることを理解していませんでした。コンパイラがオブジェクトの型を正確に追跡できなかったため、コードで未解決の参照エラーが発生することがありました。Kotlin 2.0.0では、これが修正されました。

kotlin
interface Rho {
    operator fun inc(): Sigma = TODO()
}

interface Sigma : Rho {
    fun sigma() = Unit
}

interface Tau {
    fun tau() = Unit
}

fun main(input: Rho) {
    var unknownObject: Rho = input

    // unknownObjectがTauインターフェースを継承しているかチェック
    // 注:unknownObjectがRhoとTauの両方のインターフェースを
    // 継承している可能性もあります。
    if (unknownObject is Tau) {

        // インターフェースRhoからオーバーロードされたinc()演算子を使用します。
        // Kotlin 2.0.0では、unknownObjectの型はSigmaにスマートキャストされます。
        ++unknownObject

        // Kotlin 2.0.0では、コンパイラはunknownObjectがSigma型であることを
        // 認識しているため、sigma()関数を正常に呼び出すことができます。
        unknownObject.sigma()

        // Kotlin 1.9.20では、コンパイラはinc()が呼び出されたときに
        // スマートキャストを実行しないため、コンパイラは依然として
        // unknownObjectがTau型であると考えます。sigma()関数を呼び出すと
        // コンパイルエラーが発生します。
        
        // Kotlin 2.0.0では、コンパイラはunknownObjectがSigma型であることを
        // 認識しているため、tau()関数を呼び出すとコンパイルエラーが発生します。
        unknownObject.tau()
        // Unresolved reference 'tau'

        // Kotlin 1.9.20では、コンパイラが誤ってunknownObjectがTau型であると
        // 考えるため、tau()関数を呼び出すことができますが、
        // ClassCastExceptionが発生します。
    }
}

Kotlinマルチプラットフォーム

K2コンパイラには、以下の領域でKotlinマルチプラットフォームに関連する改善があります。

コンパイル中の共通ソースとプラットフォームソースの分離

以前は、Kotlinコンパイラの設計上、コンパイル時に共通(common)ソースセットとプラットフォーム(platform)ソースセットを分離しておくことができませんでした。その結果、共通コードがプラットフォームコードにアクセスできてしまい、プラットフォーム間で異なる動作が発生することがありました。さらに、共通コードからのいくつかのコンパイラ設定や依存関係がプラットフォームコードに漏れ出すこともありました。

Kotlin 2.0.0では、新しいKotlin K2コンパイラの実装にコンパイルスキームの再設計が含まれ、共通ソースセットとプラットフォームソースセットが厳密に分離されるようになりました。この変更は、expectedおよびactual関数を使用する場合に最も顕著になります。以前は、共通コードでの関数呼び出しが、プラットフォームコード内の関数として解決される可能性がありました。例:

共通コードプラットフォームコード
kotlin
fun foo(x: Any) = println("common foo")

fun exampleFunction() {
    foo(42)
}
kotlin
// JVM
fun foo(x: Int) = println("platform foo")

// JavaScript
// JavaScriptプラットフォームにはfoo()関数のオーバーロードはありません

この例では、共通コードは実行されるプラットフォームに応じて異なる動作をします。

  • JVMプラットフォームでは、共通コードで foo() 関数を呼び出すと、プラットフォームコードの foo() 関数が platform foo として呼び出されます。
  • JavaScriptプラットフォームでは、プラットフォームコードにそのような関数が存在しないため、共通コードで foo() 関数を呼び出すと、共通コードの foo() 関数が common foo として呼び出されます。

Kotlin 2.0.0では、共通コードはプラットフォームコードにアクセスできないため、両方のプラットフォームで foo() 関数は共通コード内の foo() 関数、すなわち common foo として正常に解決されます。

プラットフォーム間の動作の一貫性の向上に加えて、IntelliJ IDEAまたはAndroid Studioとコンパイラの間で動作が矛盾していたケースの修正にも力を入れました。例えば、expectedおよびactualクラスを使用した場合、以前は以下のようになっていました。

共通コードプラットフォームコード
kotlin
expect class Identity {
    fun confirmIdentity(): String
}

fun common() {
    // 2.0.0より前では、IDE限定のエラーが発生します
    Identity().confirmIdentity()
    // RESOLUTION_TO_CLASSIFIER : Expected class Identity has no default constructor.
}
kotlin
actual class Identity {
    actual fun confirmIdentity() = "expect class fun: jvm"
}

この例では、expectedクラス Identity にデフォルトコンストラクタがないため、共通コードで正常に呼び出すことはできません。以前は、IDEによってのみエラーが報告されていましたが、JVM上ではコードは正常にコンパイルされていました。しかし、現在はコンパイラが正しくエラーを報告します。

none
Expected class 'expect class Identity : Any' does not have default constructor
解決動作が変わらない場合

新しいコンパイルスキームへの移行はまだ進行中であるため、同じソースセット内にない関数を呼び出す場合の解決動作は以前と同じです。この違いは、主に共通コードでマルチプラットフォームライブラリのオーバーロードを使用する場合に気づくでしょう。

異なるシグネチャを持つ2つの whichFun() 関数を持つライブラリがあると仮定します。

kotlin
// ライブラリの例

// MODULE: common
fun whichFun(x: Any) = println("common function") 

// MODULE: JVM
fun whichFun(x: Int) = println("platform function")

共通コードで whichFun() 関数を呼び出すと、ライブラリ内で最も関連性の高い引数型を持つ関数が解決されます。

kotlin
// JVMターゲットに例のライブラリを使用するプロジェクト

// MODULE: common
fun main(){
    whichFun(2) 
    // platform function
}

比較として、同じソースセット内で whichFun() のオーバーロードを宣言した場合、コードがプラットフォーム固有のバージョンにアクセスできないため、共通コードからの関数が解決されます。

kotlin
// 例のライブラリは使用しない

// MODULE: common
fun whichFun(x: Any) = println("common function") 

fun main(){
    whichFun(2) 
    // common function
}

// MODULE: JVM
fun whichFun(x: Int) = println("platform function")

マルチプラットフォームライブラリと同様に、commonTest モジュールは別のソースセットにあるため、依然としてプラットフォーム固有のコードにアクセスできます。したがって、commonTest モジュールでの関数呼び出しの解決は、古いコンパイルスキームと同じ動作を示します。

将来的に、これらの残りのケースも新しいコンパイルスキームとより一貫したものになる予定です。

expected宣言とactual宣言の異なる可視性レベル

Kotlin 2.0.0より前は、Kotlinマルチプラットフォームプロジェクトでexpected宣言とactual宣言を使用する場合、それらは同じ可視性レベルである必要がありました。Kotlin 2.0.0では、actual宣言がexpected宣言よりも寛容(より公開されている)である場合に限り、異なる可視性レベルもサポートされるようになりました。例:

kotlin
expect internal class Attribute // 可視性はinternal
actual class Attribute          // 可視性はデフォルトでpublic、
                                // これはより寛容です

同様に、actual宣言で型エイリアス(Type alias)を使用している場合、基になる型(Underlying type)の可視性は、expected宣言と同じか、それよりも寛容である必要があります。例:

kotlin
expect internal class Attribute                 // 可視性はinternal
internal actual typealias Attribute = Expanded

class Expanded                                  // 可視性はデフォルトでpublic、
                                                // これはより寛容です

Kotlin K2コンパイラを有効にする方法

Kotlin 2.0.0以降、Kotlin K2コンパイラはデフォルトで有効になっています。

Kotlinのバージョンをアップグレードするには、Gradle または Maven のビルドスクリプトでバージョンを2.0.0以降のリリースに変更してください。

GradleでKotlinビルドレポートを使用する

Kotlinビルドレポートは、Kotlinコンパイラタスクの異なるコンパイルフェーズに費やされた時間、使用されたコンパイラとKotlinのバージョン、およびコンパイルが増分であったかどうかに関する情報を提供します。これらのビルドレポートは、ビルドパフォーマンスの評価に役立ちます。これらはすべてのGradleタスクのパフォーマンスの概要を提供するため、GradleビルドスキャンよりもKotlinコンパイルパイプラインに関するより多くの洞察を提供します。

ビルドレポートを有効にする方法

ビルドレポートを有効にするには、gradle.properties ファイルにビルドレポートの出力保存先を宣言します。

none
kotlin.build.report.output=file

出力には以下の値とその組み合わせが使用可能です。

オプション説明
fileビルドレポートを人間が読める形式でローカルファイルに保存します。デフォルトは ${project_folder}/build/reports/kotlin-build/${project_name}-timestamp.txt です。
single_fileビルドレポートをオブジェクトの形式で指定されたローカルファイルに保存します。
build_scanビルドレポートをビルドスキャンcustom values セクションに保存します。Gradle Enterpriseプラグインはカスタム値の数と長さを制限していることに注意してください。大規模なプロジェクトでは、一部の値が失われる可能性があります。
httpHTTP(S)を使用してビルドレポートを投稿します。POSTメソッドはメトリクスをJSON形式で送信します。送信されるデータの現在のバージョンは、Kotlinリポジトリで確認できます。HTTPエンドポイントのサンプルは、こちらのブログ記事 にあります。
jsonビルドレポートをJSON形式でローカルファイルに保存します。ビルドレポートの場所は kotlin.build.report.json.directory で設定します。デフォルトの名前は ${project_name}-build-<date-time>-<index>.json です。

ビルドレポートで可能なことの詳細については、ビルドレポートを参照してください。

IDEでのサポート

IntelliJ IDEAとAndroid StudioはどちらもK2コンパイラを完全にサポートしており、デフォルトでこれを使用してコード解析、コード補完、およびハイライトを改善します。何も設定する必要はありません。最新バージョンにアップデートして、その利点を確認してください。

Kotlin PlaygroundでKotlin K2コンパイラを試す

Kotlin PlaygroundはKotlin 2.0.0以降のリリースをサポートしています。ぜひチェックしてみてください!

以前のコンパイラにロールバックする方法

Kotlin 2.0.0以降のリリースで以前のコンパイラを使用するには、以下のいずれかを行います。

変更点

新しいフロントエンドの導入により、Kotlinコンパイラはいくつかの変更を受けました。まず、コードに影響を与える最も重要な修正点を挙げ、何が変わったのか、そして今後のベストプラクティスについて説明します。さらに詳しく知りたい場合は、読み進めやすいようにこれらの変更を分野別にまとめています。

このセクションでは、以下の変更点について説明します。

バッキングフィールドを持つopenプロパティの即時初期化

何が変わったのか?

Kotlin 2.0では、バッキングフィールドを持つすべての open プロパティを即座に初期化する必要があります。そうしないと、コンパイルエラーが発生します。以前は、open var プロパティのみが即時の初期化を必要としていましたが、現在はバッキングフィールドを持つ open val プロパティにも適用されます。

kotlin
open class Base {
    open val a: Int
    open var b: Int
    
    init {
        // Kotlin 2.0以降ではエラーになりますが、以前は正常にコンパイルされていました
        this.a = 1 // エラー: open val must have initializer
        // 常にエラー
        this.b = 1 // エラー: open var must have initializer
    }
}

class Derived : Base() {
    override val a: Int = 2
    override var b = 2
}

この変更により、コンパイラの動作がより予測しやすくなります。open val プロパティがカスタムセッターを持つ var プロパティによってオーバーライドされる例を考えてみましょう。

カスタムセッターが使用される場合、バッキングフィールドを初期化したいのか、セッターを呼び出したいのかが不明確になるため、遅延初期化は混乱を招く可能性があります。以前は、セッターを呼び出したい場合、古いコンパイラはセッターがその後にバッキングフィールドを初期化することを保証できませんでした。

現在のベストプラクティスは?

バッキングフィールドを持つopenプロパティは常に初期化することをお勧めします。この習慣は効率的であり、エラーも少なくなると考えています。

ただし、プロパティを即座に初期化したくない場合は、以下のようにします。

  • プロパティを final にする。
  • 遅延初期化を可能にするプライベートなバッキングプロパティを使用する。

詳細については、YouTrackの対応する課題を参照してください。

投影されたレシーバーでの合成セッターの非推奨化

何が変わったのか?

Javaクラスの合成セッター(Synthetic setter)を使用して、クラスの投影された型(Projected type)と矛盾する型を割り当てようとすると、エラーが発生します。

getFoo() および setFoo() メソッドを含む Container という名前のJavaクラスがあるとします。

java
public class Container<E> {
    public E getFoo() {
        return null;
    }
    public void setFoo(E foo) {}
}

次のようなKotlinコードがあり、Container クラスのインスタンスが投影された型を持っている場合、setFoo() メソッドを使用すると常にエラーが発生します。しかし、Kotlin 2.0.0からのみ、合成プロパティ foo もエラーを発生させるようになります。

kotlin
fun exampleFunction(starProjected: Container<*>, inProjected: Container<in Number>, sampleString: String) {
    starProjected.setFoo(sampleString)
    // Kotlin 1.0からエラー

    // 合成セッター `foo` は `setFoo()` メソッドに解決されます
    starProjected.foo = sampleString
    // Kotlin 2.0.0からエラー

    inProjected.setFoo(sampleString)
    // Kotlin 1.0からエラー

    // 合成セッター `foo` は `setFoo()` メソッドに解決されます
    inProjected.foo = sampleString
    // Kotlin 2.0.0からエラー
}

現在のベストプラクティスは?

この変更によってコードにエラーが生じる場合は、型宣言の構成を再考することをお勧めします。型投影を使用する必要がないか、あるいはコードから代入を削除する必要があるかもしれません。

詳細については、YouTrackの対応する課題を参照してください。

アクセス不能なジェネリック型の使用禁止

何が変わったのか?

K2コンパイラの新しいアーキテクチャにより、アクセス不能なジェネリック型の処理方法を変更しました。一般的に、アクセス不能なジェネリック型に依存すべきではありません。なぜなら、これはプロジェクトのビルド設定に誤りがあることを示しており、コンパイラがコンパイルに必要な情報にアクセスできなくなっているからです。Kotlin 2.0.0では、アクセス不能なジェネリック型を使用して関数リテラルを宣言したり呼び出したりすること、またアクセス不能なジェネリック型引数を持つジェネリック型を使用することはできません。この制限は、コードの後半でコンパイラエラーが発生するのを防ぐのに役立ちます。

例えば、あるモジュールでジェネリッククラスを宣言したとします。

kotlin
// モジュール1
class Node<V>(val value: V)

別のモジュール(モジュール2)にモジュール1への依存関係が設定されている場合、コードは Node<V> クラスにアクセスし、関数型の中で型として使用できます。

kotlin
// モジュール2
fun execute(func: (Node<Int>) -> Unit) {}
// 関数は正常にコンパイルされます

しかし、プロジェクトの設定が誤っており、モジュール2にのみ依存する第3のモジュール(モジュール3)がある場合、Kotlinコンパイラはモジュール3のコンパイル時にモジュール1Node<V> クラスにアクセスできません。Kotlin 2.0.0では、Node<V> 型を使用するモジュール3内のラムダや匿名関数はエラーとなり、回避可能なコンパイラエラー、クラッシュ、実行時の例外を未然に防ぎます。

kotlin
// モジュール3
fun test() {
    // 暗黙のラムダパラメータ(it)の型がアクセス不能なNodeに解決されるため、
    // Kotlin 2.0.0ではエラーが発生します
    execute {}

    // 未使用のラムダパラメータ(_)の型がアクセス不能なNodeに解決されるため、
    // Kotlin 2.0.0ではエラーが発生します
    execute { _ -> }

    // 未使用の匿名関数パラメータ(_)の型がアクセス不能なNodeに解決されるため、
    // Kotlin 2.0.0ではエラーが発生します
    execute(fun (_) {})
}

アクセス不能なジェネリック型の値パラメータを含む関数リテラルがエラーを発生させることに加えて、型がアクセス不能なジェネリック型引数を持っている場合にもエラーが発生します。

例えば、モジュール1に同じジェネリッククラスの宣言があるとします。モジュール2では、別のジェネリッククラス Container<C> を宣言します。さらに、モジュール2で、型引数としてジェネリッククラス Node<V> を使用する Container<C> を使う関数を宣言します。

モジュール1モジュール2
kotlin
// モジュール1
class Node<V>(val value: V)
kotlin
// モジュール2
class Container<C>(vararg val content: C)

// ジェネリッククラス型を持ち、
// ジェネリッククラス型引数も持つ関数
fun produce(): Container<Node<Int>> = Container(Node(42))
fun consume(arg: Container<Node<Int>>) {}

これらの関数をモジュール3で呼び出そうとすると、ジェネリッククラス Node<V> がモジュール3からアクセス不能であるため、Kotlin 2.0.0でエラーが発生します。

kotlin
// モジュール3
fun test() {
    // ジェネリッククラスNode<V>がアクセス不能であるため、
    // Kotlin 2.0.0ではエラーが発生します
    consume(produce())
}

将来のリリースでは、アクセス不能な型の使用全般を非推奨にし続ける予定です。Kotlin 2.0.0では、非ジェネリック型を含む、アクセス不能な型を使用するいくつかのシナリオに対して警告を追加することから始めています。

例えば、前の例と同じモジュール構成で、ジェネリッククラス Node<V> を非ジェネリッククラス IntNode に変更し、すべての関数をモジュール2で宣言します。

モジュール1モジュール2
kotlin
// モジュール1
class IntNode(val value: Int)
kotlin
// モジュール2
// `IntNode`型を持つラムダパラメータを含む関数 
fun execute(func: (IntNode) -> Unit) {}

class Container<C>(vararg val content: C)

// 型引数として`IntNode`を持つ
// ジェネリッククラス型の関数
fun produce(): Container<IntNode> = Container(IntNode(42))
fun consume(arg: Container<IntNode>) {}

モジュール3でこれらの関数を呼び出すと、いくつかの警告が発生します。

kotlin
// モジュール3
fun test() {
    // クラスIntNodeがアクセス不能であるため、
    // Kotlin 2.0.0では警告が発生します。

    execute {}
    // Class 'IntNode' of the parameter 'it' is inaccessible.

    execute { _ -> }
    execute(fun (_) {})
    // Class 'IntNode' of the parameter '_' is inaccessible.

    // 将来のKotlinリリースで、IntNodeがアクセス不能である
    // ことに対する警告が発生するようになります。
    consume(produce())
}

現在のベストプラクティスは?

アクセス不能なジェネリック型の新しい警告に遭遇した場合、ビルドシステムの構成に問題がある可能性が高いです。ビルドスクリプトと構成を確認することをお勧めします。

最後の手段として、モジュール3からモジュール1への直接的な依存関係を設定することもできます。あるいは、同じモジュール内で型にアクセスできるようにコードを修正することもできます。

詳細については、YouTrackの対応する課題を参照してください。

同じ名前のKotlinプロパティとJavaフィールドの一貫した解決順序

何が変わったのか?

Kotlin 2.0.0より前、互いに継承し合い、同じ名前のKotlinプロパティとJavaフィールドを含むJavaクラスとKotlinクラスを扱っていた場合、重複した名前の解決動作に一貫性がありませんでした。また、IntelliJ IDEAとコンパイラの間で動作が矛盾していました。Kotlin 2.0.0のための新しい解決動作を開発する際、ユーザーへの影響を最小限に抑えることを目指しました。

例えば、Javaクラス Base があるとします。

java
public class Base {
    public String a = "a";

    public String b = "b";
}

前述の Base クラスを継承するKotlinクラス Derived もあるとします。

kotlin
class Derived : Base() {
    val a = "aa"

    // カスタムget()関数を宣言
    val b get() = "bb"
}

fun main() {
    // Derived.aを解決
    println(a)
    // aa

    // Base.bを解決
    println(b)
    // b
}

Kotlin 2.0.0より前では、aDerived Kotlinクラス内のKotlinプロパティに解決されますが、bBase Javaクラス内のJavaフィールドに解決されます。

Kotlin 2.0.0では、この例の解決動作が一貫したものになり、同じ名前のJavaフィールドよりもKotlinプロパティが優先されるようになります。現在、bDerived.b に解決されます。

Kotlin 2.0.0より前では、IntelliJ IDEAを使用して a の宣言や使用箇所に移動しようとすると、Kotlinプロパティに移動すべきところを誤ってJavaフィールドに移動していました。

Kotlin 2.0.0からは、IntelliJ IDEAはコンパイラと同じ場所に正しく移動します。

一般的なルールは、サブクラスが優先されるということです。前の例では、DerivedBase Javaクラスのサブクラスであるため、Derived クラスのKotlinプロパティ a が解決されることでこれを示しています。

継承が逆になり、JavaクラスがKotlinクラスを継承している場合、サブクラス内のJavaフィールドが、同じ名前を持つKotlinプロパティよりも優先されます。

この例を考えてみましょう。

KotlinJava
kotlin
open class Base {
    val a = "aa"
}
java
public class Derived extends Base {
    public String a = "a";
}

この場合、次のコードでは:

kotlin
fun main() {
    // Derived.aを解決
    println(a)
    // a
}

現在のベストプラクティスは?

この変更がコードに影響を与える場合は、重複した名前を使用する必要が本当にあるかどうかを検討してください。JavaクラスまたはKotlinクラスに同じ名前のフィールドまたはプロパティを持たせ、それぞれが他方を継承するようにしたい場合は、サブクラスのフィールドまたはプロパティが優先されることに注意してください。

詳細については、YouTrackの対応する課題を参照してください。

Javaプリミティブ配列の null 安全性の向上

何が変わったのか?

Kotlin 2.0.0から、コンパイラはKotlinにインポートされたJavaプリミティブ配列のnull許容性を正しく推論するようになりました。現在、Javaプリミティブ配列で使用されている TYPE_USE アノテーションからのネイティブなnull許容性を保持し、それらの値がアノテーションに従って使用されていない場合にエラーを生成します。

通常、@Nullable および @NotNull アノテーションを持つJava型がKotlinから呼び出されると、適切なネイティブnull許容性が付与されます。

java
interface DataService {
    @NotNull ResultContainer<@Nullable String> fetchData();
}
kotlin
val dataService: DataService = ... 
dataService.fetchData() // -> ResultContainer<String?>

以前は、Javaプリミティブ配列がKotlinにインポートされた際、すべての TYPE_USE アノテーションが失われ、プラットフォームnull許容性(Platform nullability)となり、安全でないコードになる可能性がありました。

java
interface DataProvider {
    int @Nullable [] fetchData();
}
kotlin
val dataService: DataProvider = ...
dataService.fetchData() // -> IntArray .. IntArray?
// アノテーションによれば `dataService.fetchData()` は `null` になる可能性があるにもかかわらず、エラーは発生しません
// これにより NullPointerException が発生する可能性があります
dataService.fetchData()[0]

この問題は、宣言自体のアノテーションには影響せず、TYPE_USE アノテーションにのみ影響していたことに注意してください。

現在のベストプラクティスは?

Kotlin 2.0.0では、Javaプリミティブ配列の null 安全性がKotlinの標準となったため、それらを使用している場合は新しい警告やエラーがないかコードを確認してください。

  • 明示的なnullチェックなしで @Nullable なJavaプリミティブ配列を使用したり、null非許容のプリミティブ配列を期待するJavaメソッドに null を渡そうとしたりするコードは、コンパイルに失敗するようになります。
  • @NotNull なプリミティブ配列をnullチェックと共に使用すると、"Unnecessary safe call"(不要なセーフコール)や "Comparison with null always false"(nullとの比較は常に偽)といった警告が表示されるようになります。

詳細については、YouTrackの対応する課題を参照してください。

expectedクラス内の抽象メンバに対するより厳格なルール

expectedクラスおよびactualクラスはベータ版です。これらはほぼ安定していますが、将来的に移行手順を実行する必要があるかもしれません。弊社では、お客様が行う必要のあるさらなる変更を最小限に抑えるよう最善を尽くします。

何が変わったのか?

K2コンパイラによるコンパイル中の共通ソースとプラットフォームソースの分離に伴い、expectedクラス内の抽象メンバに対してより厳格なルールを導入しました。

以前のコンパイラでは、expectedの非抽象クラスが、関数をオーバーライドすることなく抽象関数を継承することが可能でした。コンパイラが共通コードとプラットフォームコードの両方に同時にアクセスできたため、コンパイラは抽象関数に対応するオーバーライドと定義がactualクラスにあるかどうかを確認できました。

現在は共通ソースとプラットフォームソースが別々にコンパイルされるため、コンパイラがその関数が抽象ではないことを認識できるように、継承された関数はexpectedクラスで明示的にオーバーライドされる必要があります。そうしないと、コンパイラは ABSTRACT_MEMBER_NOT_IMPLEMENTED エラーを報告します。

例えば、共通ソースセットで listFiles() という抽象関数を持つ FileSystem という抽象クラスを宣言したとします。プラットフォームソースセットでは、actual宣言の一部として listFiles() 関数を定義します。

共通コードに、FileSystem クラスを継承する PlatformFileSystem という名前のexpectedの非抽象クラスがある場合、PlatformFileSystem クラスは抽象関数 listFiles() を継承します。しかし、Kotlinでは非抽象クラス内に抽象関数を持たせることはできません。listFiles() 関数を非抽象にするには、abstract キーワードなしでオーバーライドとして宣言する必要があります。

共通コードプラットフォームコード
kotlin
abstract class FileSystem {
    abstract fun listFiles()
}
expect open class PlatformFileSystem() : FileSystem {
    // Kotlin 2.0.0では、明示的なオーバーライドが必要です
    expect override fun listFiles()
    // Kotlin 2.0.0より前では、オーバーライドは不要でした
}
kotlin
actual open class PlatformFileSystem : FileSystem {
    actual override fun listFiles() {}
}

現在のベストプラクティスは?

expectedの非抽象クラスで抽象関数を継承している場合は、非抽象のオーバーライドを追加してください。

詳細については、YouTrackの対応する課題 を参照してください。

分野別

これらの分野別リストには、コードに影響を与える可能性は低いものの、詳細を確認するための関連するYouTrack課題へのリンクが含まれています。課題IDの隣にアスタリスク(*)が付いている変更は、セクションの冒頭で説明されています。

型推論

課題 IDタイトル
KT-64189型が明示的に Normal である場合、プロパティ参照のコンパイル済み関数シグネチャにおける型が正しくない
KT-47986ビルダー推論コンテキストにおいて、型変数を上限(upper bound)へ暗黙的に推論することを禁止する
KT-59275K2: 配列リテラル内のジェネリックアノテーション呼び出しに明示的な型引数を要求する
KT-53752交差型(intersection type)に対するサブタイピングチェックの漏れ
KT-59138Kotlin における Java 型パラメータに基づく型のデフォルト表現を変更
KT-57178前置インクリメントの推論される型を、inc() 演算子の戻り値型ではなくゲッターの戻り値型を返すように変更
KT-57609K2: 反変(contravariant)パラメータに対する @UnsafeVariance の存在への依存を停止
KT-57620K2: raw 型に対して subsumed メンバへの解決を禁止
KT-64641K2: 拡張関数パラメータを持つ呼び出し可能オブジェクトへの参照の型を正しく推論
KT-57011指定された場合、分解変数の実際の型を明示的な型と一致させる
KT-38895K2: フィックス:整数リテラルのオーバーフローに関する不整合な動作
KT-54862匿名型が型引数から匿名関数を介して公開される可能性がある
KT-22379break を伴う while ループの条件が、不適切なスマートキャストを生成する可能性がある
KT-62507K2: expect/actual トップレベルプロパティに対する共通コードでのスマートキャストを禁止
KT-65750戻り値の型を変更するインクリメントおよびプラス演算子がスマートキャストに影響を与える必要がある
KT-65349[LC] K2: 変数型を明示的に指定すると、K1 で動作していたいくつかのケースで束縛スマートキャストが壊れる

ジェネリクス

課題 IDタイトル
KT-54309*投影されたレシーバーでの合成セッターの使用を非推奨化
KT-57600raw 型パラメータを持つ Java メソッドを、ジェネリック型パラメータでオーバーライドすることを禁止
KT-54663null 許容の可能性がある型パラメータを in 投影された DNN パラメータに渡すことを禁止
KT-54066typealias コンストラクタにおける上限(upper bound)違反を非推奨化
KT-49404Java クラスに基づく反変キャプチャ型の型の不健全性を修正
KT-61718自己上限(self upper bounds)とキャプチャ型を持つ不健全なコードを禁止
KT-61749ジェネリック外部クラスのジェネリック内部クラスにおける不健全な境界違反を禁止
KT-62923K2: 内部クラスの外部スーパータイプの投影に対して PROJECTION_IN_IMMEDIATE_ARGUMENT_TO_SUPERTYPE を導入
KT-63243別のスーパータイプから追加の特化された実装を持つプリミティブのコレクションを継承する際に MANY_IMPL_MEMBER_NOT_IMPLEMENTED を報告
KT-60305K2: 展開された型に変異修飾子(variance modifiers)を持つ型エイリアスでのコンストラクタ呼び出しと継承を禁止
KT-64965自己上限を持つキャプチャ型の不適切な処理によって生じる型の穴を修正
KT-64966ジェネリックパラメータに対して誤った型を持つジェネリック委譲コンストラクタ呼び出しを禁止
KT-65712上限がキャプチャ型である場合の欠落した上限違反を報告

解決(Resolution)

課題 IDタイトル
KT-55017*Java フィールドを持つベースクラスとのオーバーロード解決中に、派生クラスの Kotlin プロパティを選択
KT-58260invoke 規約が期待される脱糖(desugaring)と一貫して動作するようにする
KT-62866K2: コンパニオンオブジェクトが静的スコープよりも優先される場合の修飾子解決の動作を変更
KT-57750型を解決する際、同じ名前のクラスがスターインポートされている場合に曖昧さエラーを報告
KT-63558K2: COMPATIBILITY_WARNING 周辺の解決を移行
KT-51194依存関係クラスが同じ依存関係の 2 つの異なるバージョンに含まれている場合の偽陰性 CONFLICTING_INHERITED_MEMBERS
KT-37592レシーバーを持つ関数型のプロパティ invoke が拡張関数の invoke よりも優先される
KT-51666修飾された this: 型で修飾された this ケースを導入/優先順位付け
KT-54166クラスパスにおける完全修飾名(FQ name)の競合が発生した場合の未指定の動作を確認
KT-64431K2: インポートにおける修飾子として型エイリアスを使用することを禁止
KT-56520K1/K2: 低いレベルで曖昧さがある型参照に対する解決タワーの誤った動作

可視性

課題 IDタイトル
KT-64474*アクセス不能な型の使用を未指定の動作として宣言
KT-55179internal インライン関数からプライベートクラスのコンパニオンオブジェクトメンバを呼び出す際の偽陰性 PRIVATE_CLASS_MEMBER_FROM_INLINE
KT-58042オーバーライドされた宣言が可視であっても、同等のゲッターが不可視であれば合成プロパティを不可視にする
KT-64255別のモジュールの派生クラスから internal セッターにアクセスすることを禁止
KT-33917プライベートインライン関数から匿名型を公開することを禁止
KT-54997パブリック API インライン関数からの暗黙的な非パブリック API アクセスを禁止
KT-56310スマートキャストが protected メンバの可視性に影響を与えないようにする
KT-65494パブリックインライン関数から、見落とされていたプライベート演算子関数へのアクセスを禁止
KT-65004K1: protected val をオーバーライドする var のセッターが public として生成される
KT-64972Kotlin/Native のリンク時においてプライベートメンバによるオーバーライドを禁止

アノテーション

課題 IDタイトル
KT-58723EXPRESSION ターゲットを持たないアノテーションで文(statement)を修飾することを禁止
KT-49930`REPEATED_ANNOTATION` チェック中に括弧式を無視する
KT-57422K2: プロパティゲッターに対する使用部位 'get' ターゲットアノテーションの使用を禁止
KT-46483where 句内の型パラメータに対するアノテーションを禁止
KT-64299コンパニオンオブジェクト上のアノテーションの解決においてコンパニオンスコープが無視される
KT-64654K2: ユーザー指定のアノテーションとコンパイラが要求するアノテーションの間に曖昧さを導入
KT-64527列挙型の値に対するアノテーションを列挙値クラスにコピーしないようにする
KT-63389K2: `()?` でラップされた型に対する互換性のないアノテーションに対して `WRONG_ANNOTATION_TARGET` が報告される
KT-63388K2: catch パラメータ型の型アノテーションに対して `WRONG_ANNOTATION_TARGET` が報告される

null 安全性

課題 IDタイトル
KT-54521*Java で Nullable とアノテーションされた配列型の安全でない使用を非推奨化
KT-41034K2: セーフコールと規約演算子の組み合わせに対する評価セマンティクスを変更
KT-50850スーパータイプの順序が継承された関数の null 許容性パラメータを定義する
KT-53982パブリックシグネチャでローカル型を近似する際、null 許容性を保持する
KT-62998安全でない代入のセレクタとして、null 許容型を null 非許容の Java フィールドに代入することを禁止
KT-63209警告レベルの Java 型に対するエラーレベルの null 許容引数の欠落したエラーを報告

Java 相互運用性

課題 IDタイトル
KT-53061ソース内で同じ完全修飾名(FQ name)を持つ Java および Kotlin クラスを禁止
KT-49882Java コレクションを継承したクラスが、スーパータイプの順序に応じて矛盾した動作をする
KT-66324K2: Kotlin のプライベートクラスを Java クラスが継承した場合の未指定の動作
KT-66220Java の可変長引数メソッドをインライン関数に渡すと、実行時にただの配列ではなく配列の配列になる
KT-66204K-J-K 階層における internal メンバのオーバーライドを許可

プロパティ

課題 IDタイトル
KT-57555*[LC] バッキングフィールドを持つ open プロパティの遅延初期化を禁止
KT-58589基本コンストラクタが存在しない場合やクラスがローカルな場合に、欠落した MUST_BE_INITIALIZED を非推奨化
KT-64295プロパティに対する潜在的な invoke 呼び出しが発生した場合の再帰的解決を禁止
KT-57290ベースクラスが別のモジュールにある場合、不可視の派生クラスからのベースクラスプロパティに対するスマートキャストを非推奨化
KT-62661K2: データクラスのプロパティに対する OPT_IN_USAGE_ERROR の漏れ

制御フロー

課題 IDタイトル
KT-56408K1 と K2 の間におけるクラス初期化ブロック内の CFA(制御フロー解析)ルールの不整合
KT-57871括弧内の else ブランチのない if 条件文における K1/K2 の不整合
KT-42995スコープ関数内での初期化を伴う try/catch ブロックにおける偽陰性 "VAL_REASSIGNMENT"
KT-65724try ブロックから catch および finally ブロックへのデータフロー情報の伝播

列挙型クラス

課題 IDタイトル
KT-57608列挙型エントリの初期化中に列挙型クラスのコンパニオンオブジェクトへのアクセスを禁止
KT-34372列挙型クラスにおける仮想インラインメソッドの欠落したエラーを報告
KT-52802プロパティ/フィールドと列挙型エントリの間の解決の曖昧さを報告
KT-47310コンパニオンプロパティが列挙型エントリよりも優先される場合の修飾子解決の動作を変更

関数型(SAM)インターフェース

課題 IDタイトル
KT-52628アノテーションなしで OptIn を必要とする SAM コンストラクタの使用を非推奨化
KT-57014JDK 関数インターフェースの SAM コンストラクタに対して、ラムダから誤った null 許容性を持つ値を返すことを禁止
KT-64342呼び出し可能参照のパラメータ型の SAM 変換が CCE(ClassCastException)を引き起こす

コンパニオンオブジェクト

課題 IDタイトル
KT-54316コンパニオンオブジェクトのメンバへの呼び出し外の参照が、無効なシグネチャを持つ
KT-47313V がコンパニオンを持つ場合の (V)::foo 参照解決の変更

その他

課題 IDタイトル
KT-59739*実装が actual 側にある場合、共通コードの継承クラスに対して K2/MPP が [ABSTRACT_MEMBER_NOT_IMPLEMENTED] を報告する
KT-49015修飾された this: 潜在的なラベルの競合が発生した場合の動作変更
KT-56545Java サブクラスで偶然重複したオーバーロードが発生した場合の、JVM バックエンドにおける誤った関数のマングリングを修正
KT-62019[LC issue] 文(statement)の位置における suspend マーク付きの匿名関数宣言を禁止
KT-55111OptIn: マーカー下でのデフォルト引数(デフォルト値を持つパラメータ)を伴うコンストラクタ呼び出しを禁止
KT-61182変数上の式 + invoke 解決に対して、Unit 変換が誤って許可されていた問題を修正
KT-55199適応を伴う呼び出し可能参照を KFunction へ昇格させることを禁止
KT-65776[LC] K2 が `false && ...` および `false || ...` を壊す問題を修正
KT-65682[LC] `header`/`impl` キーワードを非推奨化
KT-45375デフォルトですべての Kotlin ラムダを invokedynamic + LambdaMetafactory を介して生成

Kotlin リリースとの互換性

以下の Kotlin リリースは、新しい K2 コンパイラをサポートしています。

Kotlin リリース安定性レベル
2.0.0–2.3.0安定版(Stable)
1.9.20–1.9.25ベータ版(Beta)
1.9.0–1.9.10JVM はベータ版
1.7.0–1.8.22アルファ版(Alpha)

Kotlin ライブラリとの互換性

Kotlin/JVM を使用している場合、K2 コンパイラは任意のバージョンの Kotlin でコンパイルされたライブラリと動作します。

Kotlin マルチプラットフォームを使用している場合、K2 コンパイラは Kotlin バージョン 1.9.20 以降でコンパイルされたライブラリと動作することが保証されています。

コンパイラプラグインのサポート

現在、Kotlin K2 コンパイラは以下の Kotlin コンパイラプラグインをサポートしています。

さらに、Kotlin K2 コンパイラは以下もサポートしています。

他のコンパイラプラグインを使用している場合は、それらのドキュメントを確認して、K2 と互換性があるかどうかを確かめてください。

カスタムコンパイラプラグインのアップグレード

カスタムコンパイラプラグインは、試験的(Experimental) なプラグイン API を使用しています。そのため、API はいつでも変更される可能性があり、後方互換性は保証されません。

アップグレードプロセスは、お使いのカスタムプラグインのタイプに応じて 2 つのパスがあります。

バックエンド限定のコンパイラプラグイン

プラグインが IrGenerationExtension 拡張ポイントのみを実装している場合、プロセスは他の新しいコンパイラリリースの場合と同じです。使用している API に変更がないか確認し、必要に応じて変更を加えてください。

バックエンドおよびフロントエンドのコンパイラプラグイン

プラグインがフロントエンド関連の拡張ポイントを使用している場合は、新しい K2 コンパイラ API を使用してプラグインを書き直す必要があります。新しい API の概要については、FIR Plugin API を参照してください。

カスタムコンパイラプラグインのアップグレードについて質問がある場合は、Slack の #compiler チャネルに参加してください。可能な限りお手伝いさせていただきます。

新しい K2 コンパイラに関するフィードバックをお寄せください

皆様からのフィードバックをお待ちしております!