Skip to content

K2コンパイラー移行ガイド

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

Kotlin K2 compiler architecture

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

新しいアーキテクチャと豊富なデータ構造により、K2コンパイラーは以下の利点を提供します。

  • 呼び出し解決と型推論の改善。コンパイラーの動作が一貫し、コードをよりよく理解します。
  • 新しい言語機能の糖衣構文の導入が容易に。将来的には、新しい機能が導入される際に、より簡潔で読みやすいコードを使用できるようになります。
  • コンパイル時間の高速化。コンパイル時間は大幅に高速化されます
  • IDEパフォーマンスの向上。2025.1以降、IntelliJ IDEAはK2モードを使用してKotlinコードを分析し、安定性を高め、パフォーマンスを向上させます。詳細については、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 Multiplatformに関連する言語機能を改善します。

スマートキャスト

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演算子 (||) と組み合わせると、それらの最も近い共通のスーパータイプにスマートキャストが行われます。この変更以前は、スマートキャストは常に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()
    }
}

共通スーパータイプは、共用体型近似です。共用体型は現在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()
        // 未解決の参照 'tau'

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

Kotlin Multiplatform

K2コンパイラーには、Kotlin Multiplatformに関連する以下の分野で改善があります。

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

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

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クラスIdentityにはデフォルトコンストラクターがない。
}
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
// 例のライブラリ

// モジュール: common
fun whichFun(x: Any) = println("common function") 

// モジュール: JVM
fun whichFun(x: Int) = println("platform function")

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

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

// モジュール: common
fun main(){
    whichFun(2) 
    // platform function
}

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

kotlin
// 例のライブラリは使用されていない

// モジュール: common
fun whichFun(x: Any) = println("common function") 

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

// モジュール: JVM
fun whichFun(x: Int) = println("platform function")

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

将来的には、これらの残りのケースも新しいコンパイルスキームとより一貫性を持つようになるでしょう。

expectedおよびactual宣言の異なる可視性レベル

Kotlin 2.0.0より前は、Kotlin Multiplatformプロジェクトでexpectedおよびactual宣言を使用する場合、それらは同じ可視性レベルを持つ必要がありました。Kotlin 2.0.0では、異なる可視性レベルもサポートするようになりましたが、これはactual宣言がexpected宣言よりも_許容範囲が広い_場合にのみです。例えば:

kotlin
expect internal class Attribute // 可視性はinternal
actual class Attribute          // 可視性はデフォルトでpublicで、
                                // より許容範囲が広い

同様に、actual宣言で型エイリアスを使用している場合、基になる型の可視性は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以降に変更してください。

IntelliJ IDEAまたはAndroid Studioで最高の体験をするには、IDEでK2モードを有効にすることを検討してください。

以前のIDEの動作

以前のIDEの動作に戻したい場合は、K2モードを無効にできます。

  1. Settings | Languages & Frameworks | Kotlin に移動します。
  2. Enable K2 mode オプションの選択を解除します。

Kotlin 2.1.0以降でStableな言語機能を導入する予定です。それまでは、コード解析のために以前のIDE機能を使い続けることができ、認識されない言語機能によるコードハイライトの問題に遭遇することはないでしょう。

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

Kotlin Playgroundは、Kotlin 2.0.0以降のリリースをサポートしています。試してみましょう!

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

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

  • build.gradle.ktsファイルで、言語バージョン1.9に設定します。

    または

  • 以下のコンパイラーオプションを使用します:-language-version 1.9

変更点

新しいフロントエンドの導入により、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 //Error: open val must have initializer
        // 常にエラー
        this.b = 1 // Error: open var must have initializer
    }
}

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

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

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

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

バッキングフィールドを持つopenプロパティは常に初期化することを推奨します。これは、より効率的でエラーが発生しにくいプラクティスであると我々は考えています。

ただし、プロパティをすぐに初期化しない場合は、次のことができます。

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

詳細については、YouTrackの対応するイシューを参照してください。

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

変更点

Javaクラスの合成セッターを使用して、クラスの投影型と競合する型を割り当てると、エラーがトリガーされます。

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

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

Containerクラスのインスタンスが投影型を持つ以下のKotlinコードがある場合、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)

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

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

ただし、プロジェクトが誤って設定されており、モジュール2のみに依存する第3のモジュール(モジュール3)がある場合、Kotlinコンパイラーはモジュール3をコンパイルする際にモジュール1Node<V>クラスにアクセスできなくなります。現在、Node<V>型を使用するモジュール3内のラムダまたは匿名関数は、Kotlin 2.0.0でエラーをトリガーし、これにより後でコードで回避可能なコンパイラーエラー、クラッシュ、および実行時例外を防ぎます。

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 {}
    // パラメータ 'it' のクラス 'IntNode' はアクセスできません。

    execute { _ -> }
    execute(fun (_) {})
    // パラメータ '_' のクラス 'IntNode' はアクセスできません。

    // IntNodeがアクセス不可能なため、
    // 将来のKotlinリリースで警告がトリガーされるでしょう。
    consume(produce())
}

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

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

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

詳細については、YouTrackの対応するイシューを参照してください。

KotlinプロパティとJavaフィールドが同じ名前を持つ場合の解決順序の一貫性

変更点

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

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

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

    public String b = "b";
}

また、前述のBaseクラスを継承するDerivedというKotlinクラスがあるとします。

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では、例での解決動作は一貫しており、Kotlinプロパティが同じ名前のJavaフィールドを置き換えることを保証します。現在、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許容性となり、安全でないコードにつながる可能性がありました。

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

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

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

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

  • 明示的なnull許容性チェックなしで@Nullable Javaプリミティブ配列を使用するコード、またはnull許容でないプリミティブ配列を期待するJavaメソッドにnullを渡そうとするコードは、これからはコンパイルに失敗します。
  • @NotNullプリミティブ配列をnull許容性チェックとともに使用すると、「不必要なセーフコール」または「nullとの比較は常にfalse」の警告が発せられます。

詳細については、YouTrackの対応するイシューを参照してください。

expectedクラスにおける抽象メンバーのより厳格なルール

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

変更点

K2コンパイラーを使用したコンパイル時の共通ソースとプラットフォームソースの分離により、expectedクラスの抽象メンバーに対してより厳格なルールを実装しました。

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

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

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

共通コードで、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イシューへのリンクを提供する変更点がリストされています。Issue IDの横にアスタリスク (*) が付いている変更は、セクションの冒頭で説明されています。

型推論

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

ジェネリクス

Issue IDタイトル
KT-54309*投影されたレシーバー上の合成セッターの使用を非推奨にする
KT-57600生型パラメータを持つJavaメソッドをジェネリック型パラメータでオーバーライドすることを禁止する
KT-54663null許容性のある型パラメータを`in`投影されたDNNパラメータに渡すことを禁止する
KT-54066型エイリアスのコンストラクターにおける上限違反を非推奨にする
KT-49404Javaクラスに基づく反変なキャプチャ型に対する型不正を修正する
KT-61718自己上限とキャプチャ型を持つ不正なコードを禁止する
KT-61749ジェネリックアウタークラスのジェネリックインナークラスにおける不正なバウンド違反を禁止する
KT-62923K2: インナークラスの外側のスーパータイプの投影に対するPROJECTION_IN_IMMEDIATE_ARGUMENT_TO_SUPERTYPEを導入する
KT-63243別のスーパータイプからの追加の特殊化された実装を持つプリミティブのコレクションから継承する場合にMANY_IMPL_MEMBER_NOT_IMPLEMENTEDを報告する
KT-60305K2: 展開型に共変性修飾子を持つ型エイリアスでのコンストラクター呼び出しと継承を禁止する
KT-64965自己上限を持つキャプチャ型の不適切な処理によって引き起こされる型ホールの修正
KT-64966ジェネリックパラメータに誤った型を持つジェネリック委譲コンストラクター呼び出しを禁止する
KT-65712上限がキャプチャ型である場合に、不足している上限違反を報告する

解決

Issue IDタイトル
KT-55017*オーバーロード解決時に、基底クラスのJavaフィールドよりも派生クラスのKotlinプロパティを選択する
KT-58260invoke規約が期待される脱糖と一貫して動作するようにする
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名競合の場合の未指定動作を確認する
KT-64431K2: インポートで型エイリアスを修飾子として使用することを禁止する
KT-56520K1/K2: 下位レベルで曖昧性を持つ型参照に対する解決タワーの誤動作

可視性

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

アノテーション

Issue IDタイトル
KT-58723EXPRESSIONターゲットを持たないアノテーションでステートメントをアノテーション付けすることを禁止する
KT-49930`REPEATED_ANNOTATION`チェック中に括弧式を無視する
KT-57422K2: プロパティゲッターに対する使用箇所 'get' ターゲットアノテーションを禁止する
KT-46483where句の型パラメータに対するアノテーションを禁止する
KT-64299コンパニオンオブジェクトに対するアノテーションの解決において、コンパニオンスコープが無視される
KT-64654K2: ユーザーとコンパイラー必須のアノテーション間に曖昧性が導入された
KT-64527enum値のアノテーションはenum値クラスにコピーされるべきではない
KT-63389K2: `()?`でラップされた型の互換性のないアノテーションに対して`WRONG_ANNOTATION_TARGET`が報告される
KT-63388K2: catchパラメータ型の注釈に対して`WRONG_ANNOTATION_TARGET`が報告される

null安全性

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

Java相互運用性

Issue IDタイトル
KT-53061ソース内で同じFQ名を持つJavaとKotlinのクラスを禁止する
KT-49882Javaコレクションから継承されたクラスは、スーパタイプの順序によって一貫性のない動作をする
KT-66324K2: KotlinプライベートクラスからのJavaクラス継承の場合における未指定動作
KT-66220Javaの可変長引数メソッドをインライン関数に渡すと、実行時に単一の配列ではなく配列の配列になる
KT-66204K-J-K階層で内部メンバーをオーバーライドできるようにする

プロパティ

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

制御フロー

Issue IDタイトル
KT-56408K1とK2間でのクラス初期化ブロックにおけるCFAの一貫性のないルール
KT-57871括弧内のelse分岐がないif条件におけるK1/K2の不整合
KT-42995スコープ関数で初期化されたtry/catchブロックでの"VAL_REASSIGNMENT"の偽陰性
KT-65724tryブロックからcatchおよびfinallyブロックへのデータフロー情報を伝播する

Enumクラス

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

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

Issue IDタイトル
KT-52628アノテーションなしでOptInを必要とするSAMコンストラクターの使用を非推奨にする
KT-57014JDK関数インターフェースのSAMコンストラクターに対するラムダから不正なnull許容値で値を返すことを禁止する
KT-64342呼び出し可能参照のパラメータ型のSAM変換がCCEにつながる

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

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

その他

Issue IDタイトル
KT-59739*K2/MPPは、共通コードの継承者に対して、実装が実際の対応する箇所にある場合に[ABSTRACT_MEMBER_NOT_IMPLEMENTED]を報告する
KT-49015修飾されたthis: 潜在的なラベル競合の場合に動作を変更する
KT-56545Javaサブクラスにおける意図しない競合オーバーロードの場合のJVMバックエンドでの不正な関数マングリングを修正する
KT-62019[LCの問題] ステートメント位置での中断マークされた匿名関数の宣言を禁止する
KT-55111OptIn: マーカーの下でデフォルト引数(デフォルト値を持つパラメータ)を持つコンストラクター呼び出しを禁止する
KT-61182変数に対する式とinvoke解決に対して、誤ってUnit変換が許可される
KT-65776[LC] K2は`false && ...`および`false || ...`を破壊する
KT-65682[LC] `header`/`impl`キーワードを非推奨にする
KT-45375デフォルトでinvokedynamic + LambdaMetafactoryを介してすべてのKotlinラムダを生成する

Kotlinリリースとの互換性

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

Kotlinリリース安定性レベル
2.0.0–2.2.10Stable
1.9.20–1.9.25Beta
1.9.0–1.9.10JVMはBeta
1.7.0–1.8.22Alpha

Kotlinライブラリとの互換性

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

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

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

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

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

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

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

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

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

バックエンドのみのコンパイラープラグイン

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

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

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

カスタムコンパイラープラグインのアップグレードに関する質問がある場合は、弊社の#compiler Slackチャンネルに参加してください。最善を尽くしてサポートします。

新しいK2コンパイラーに関するフィードバックを共有する

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