Skip to content

K2 编译器迁移指南

随着 Kotlin 语言和生态系统的持续演进,Kotlin 编译器也随之发展。第一步是引入新的 JVM 和 JS IR(中间表示)后端,它们共享逻辑,简化了针对不同平台目标的代码生成。现在,其演进的下一个阶段带来了名为 K2 的新前端。

Kotlin K2 编译器架构

随着 K2 编译器的到来,Kotlin 前端已被彻底重写,并采用了全新、更高效的架构。新编译器带来的根本性变化是使用了包含更多语义信息的统一数据结构。此前端负责执行语义分析、调用解析类型推断

新架构和丰富的数据结构使 K2 编译器能够提供以下益处:

  • 改进的调用解析和类型推断。编译器行为更一致,对你的代码理解更深入。
  • 更容易引入新语言特性的语法糖。未来,当新特性引入时,你将能够使用更简洁、更可读的代码。
  • 更快的编译时间编译时间可以显著加快
  • 增强的 IDE 性能。从 2025.1 开始,IntelliJ IDEA 使用 K2 模式分析你的 Kotlin 代码,提升了稳定性并提供了性能改进。有关更多信息,请参见IDE 支持

本指南:

  • 阐释了新 K2 编译器的益处。
  • 强调你在迁移过程中可能遇到的变化,以及如何相应地调整代码。
  • 描述了如何回滚到之前的版本。

新 K2 编译器从 2.0.0 开始默认启用。有关 Kotlin 2.0.0 中提供的新特性以及新 K2 编译器的更多信息,请参见 Kotlin 2.0.0 的新增特性

性能改进

为了评估 K2 编译器的性能,我们在两个开源项目上运行了性能测试: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 开始,如果你在使用变量之前在 ifwhenwhile 条件中声明它,那么编译器收集到的任何关于该变量的信息都将可在相应的代码块中用于智能类型转换

这在你想要做的事情上会很有用,例如将布尔条件提取到变量中。然后,你可以给变量一个有意义的名称,这将提高你的代码可读性,并使得稍后在代码中重用该变量成为可能。例如:

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
}

使用逻辑或操作符的类型检测

在 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() 函数会触发
        // 未解析引用错误。signal() 函数只有在 
        // 另一次类型检测之后才能成功调用:
        
        // check(signalStatus is Status)
        // signalStatus.signal()
    }
}

共同父类型是联合类型近似。Kotlin 目前不支持联合类型

内联函数

在 Kotlin 2.0.0 中,K2 编译器对待内联函数的方式不同,使其能够结合其他编译器分析,判断是否可以安全地进行智能类型转换

具体来说,内联函数现在被视为具有隐式 callsInPlace契约。这意味着传递给内联函数的任何 lambda 函数都会在原位调用。由于 lambda 函数在原位调用,编译器知道 lambda 函数不会泄露对其函数体内任何变量的引用。

编译器将此知识与其它编译器分析结合使用,以决定是否可以安全地对任何捕获的变量进行智能类型转换。例如:

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 版本中,曾有一个 bug,导致带有函数类型的类属性无法进行智能类型转换。我们在 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 中,我们改进了异常处理,以便智能类型转换信息可以传递到 catchfinally 代码块。此更改使你的代码更安全,因为编译器会跟踪你的对象是否是可空的类型。例如:

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 保持可空的。
        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 Multiplatform

K2 编译器在以下领域改进了 Kotlin Multiplatform 相关特性

编译项期间公共和平台源代码的分离

之前,Kotlin 编译器的设计阻止了它在编译期将公共和平台源代码集分开。因此,公共代码可以访问平台代码,导致平台之间行为不一致。此外,编译器设置和公共代码中的依赖项会泄露到平台代码中。

在 Kotlin 2.0.0 中,我们新 Kotlin K2 编译器的实现包含对编译方案的重新设计,以确保公共和平台源代码集之间的严格分离。当你使用 expectactual 函数时,此更改最为明显。之前,公共代码中的函数调用可能会解析为平台代码中的函数。例如:

公共代码平台代码
kotlin
fun foo(x: Any) = println("common foo")

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

// JavaScript
// There is no foo() function overload on the JavaScript platform

在此示例中,公共代码的行为因其运行的平台而异:

  • 在 JVM 平台上,在公共代码中调用 foo() 函数会导致平台代码中的 foo() 函数被调用,作为 platform foo
  • 在 JavaScript 平台上,在公共代码中调用 foo() 函数会导致公共代码中的 foo() 函数被调用,作为 common foo,因为平台代码中没有这样的函数可用。

在 Kotlin 2.0.0 中,公共代码无法访问平台代码,因此两个平台都成功将 foo() 函数解析为公共代码中的 foo() 函数common foo

除了改进了跨平台行为的一致性之外,我们还努力修复了 IntelliJ IDEA 或 Android Studio 与编译器之间行为冲突的情况。例如,当你使用 expectactual时,会发生以下情况:

公共代码平台代码
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"
}

在此示例中,expectIdentity 没有默认构造函数,因此无法在公共代码中成功调用。之前,只有 IDE 报告错误,但代码在 JVM 上仍然成功编译。然而,现在编译器会正确报告错误:

none
Expected class 'expect class Identity : Any' does not have default constructor
何时解析行为不变

我们仍在向新的编译方案迁移中,因此当你调用不在同一源代码集中的函数时,解析行为仍然相同。你主要会在公共代码中使用多平台库中的重载时注意到这种差异。

假设你有一个库,它有两个带有不同签名的 whichFun() 函数

kotlin
// Example library

// 模块: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 模块中函数的解析行为与旧的编译方案相同。

未来,这些剩余情况将与新的编译方案更加一致。

expectactual 声明的不同可见性级别

在 Kotlin 2.0.0 之前,如果你在 Kotlin Multiplatform 项目中使用了 expectactual 声明,它们必须具有相同的可见性级别。Kotlin 2.0.0 现在也支持不同的可见性级别,但仅限 actual 声明expect 声明更宽松的情况。例如:

kotlin
expect internal class Attribute // 可见性为 internal
actual class Attribute          // 默认可见性为 public,
                                // 更宽松

同样,如果你在 actual 声明中使用了类型别名,则底层类型的可见性应与 expect 声明相同或更宽松。例如:

kotlin
expect internal class Attribute                 // 可见性为 internal
internal actual typealias Attribute = Expanded

class Expanded                                  // 默认可见性为 public,
                                                // 更宽松

如何启用 Kotlin K2 编译器

从 Kotlin 2.0.0 开始,Kotlin K2 编译器默认启用。

要升级 Kotlin 版本,请在你的 GradleMaven构建脚本中将其更改为 2.0.0 或更高版本。

为了在 IntelliJ IDEA 或 Android Studio 中获得最佳体验,请考虑在 IDE 中启用 K2 模式

将 Kotlin 构建报告与 Gradle 结合使用

Kotlin 构建报告提供了关于 Kotlin 编译器任务在不同编译项阶段所花费时间的信息,以及使用了哪个编译器和 Kotlin 版本,以及编译项是否为增量编译项。这些构建报告对于评估你的构建性能很有用。它们比 Gradle 构建扫描 对 Kotlin 编译项流水线有更多洞察,因为它们为你提供了所有 Gradle 任务的性能概览。

如何启用构建报告

要启用构建报告,请在你的 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 插件限制了自定义值的数量和长度。在大型项目中,某些值可能会丢失。
http使用 HTTP(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 模式使用 K2 编译器来改进代码分析、代码补全和高亮显示。

从 IntelliJ IDEA 2025.1 开始,K2 模式默认启用

在 Android Studio 中,你可以从 2024.1 开始按照以下步骤启用 K2 模式:

  1. 转到 设置 | 语言和框架 | Kotlin
  2. 选择启用 K2 模式选项。

之前的 IDE 行为

如果你想恢复之前的 IDE 行为,可以禁用 K2 模式:

  1. 转到 设置 | 语言和框架 | Kotlin
  2. 取消选择启用 K2 模式选项。

我们计划在 Kotlin 2.1.0 之后引入稳定语言特性。在此之前,你可以继续使用之前的 IDE 特性进行代码分析,并且不会遇到因未识别的语言特性而导致的任何代码高亮问题。

在 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 //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 属性被带有自定义settervar 属性覆盖

如果使用自定义setter,延迟初始化可能导致混淆,因为不清楚你是想初始化幕后字段还是调用setter。过去,如果你想调用setter,旧编译器无法保证setter会初始化幕后字段

现在最佳实践是什么?

我们鼓励你始终初始化带有幕后字段open 属性,因为我们相信这种做法更高效且不易出错。

但是,如果你不想立即初始化属性,可以:

  • 将属性设为 final
  • 使用允许延迟初始化的私有幕后属性

有关更多信息,请参见 YouTrack 中的相应问题。

弃用对型变接收者使用合成setter

更改了什么?

如果你使用 Java 类的合成setter赋值一个与类型变类型冲突的类型,就会触发错误。

假设你有一个名为 Container 的 Java 类,它包含 getFoo()setFoo() 方法

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 开始报错

    // 合成 setter `foo` 解析为 `setFoo()` 方法
    starProjected.foo = sampleString
    // 从 Kotlin 2.0.0 开始报错

    inProjected.setFoo(sampleString)
    // 从 Kotlin 1.0 开始报错

    // 合成 setter `foo` 解析为 `setFoo()` 方法
    inProjected.foo = sampleString
    // 从 Kotlin 2.0.0 开始报错
}

现在最佳实践是什么?

如果你发现此更改导致代码中出现错误,你可能需要重新考虑如何声明你的类型。你可能不需要使用类型型变,或者你可能需要从代码中删除任何赋值

有关更多信息,请参见 YouTrack 中的相应问题。

禁止使用无法访问的泛型类型

更改了什么?

由于 K2 编译器的新架构,我们更改了处理无法访问的泛型类型的方式。通常,你不应该在代码中依赖无法访问的泛型类型,因为这表明你的项目构建配置存在配置错误,导致编译器无法访问编译所需的必要信息。在 Kotlin 2.0.0 中,你无法声明或调用带有无法访问的泛型类型的函数字面量,也无法使用带有无法访问的泛型类型实参的泛型类型。此限制有助于你避免代码后期出现编译器错误。

例如,假设你在一个模块中声明了一个泛型类:

kotlin
// 模块一
class Node<V>(val value: V)

如果你有另一个模块(模块二),其依赖项配置在模块一上,你的代码可以访问 Node<V> 类并将其用作函数类型中的类型:

kotlin
// 模块二
fun execute(func: (Node<Int>) -> Unit) {}
// 函数编译成功

然而,如果你的项目配置错误,使得你有一个只依赖模块二的第三方模块(模块三),那么 Kotlin 编译器在编译模块三时将无法访问模块一中的 Node<V> 类。现在,模块三中任何使用 Node<V> 类型的 lambda 或匿名函数都会在 Kotlin 2.0.0 中触发错误,从而避免了代码后期可能出现的编译器错误、崩溃和运行时异常:

kotlin
// 模块三
fun test() {
    // 在 Kotlin 2.0.0 中触发错误,因为隐式
    // lambda 形参 (it) 的类型解析为 Node,它是无法访问的
    execute {}

    // 在 Kotlin 2.0.0 中触发错误,因为未使用的
    // lambda 形参 (_) 的类型解析为 Node,它是无法访问的
    execute { _ -> }

    // 在 Kotlin 2.0.0 中触发错误,因为未使用的
    // 匿名函数形参 (_) 的类型解析为 Node,它是无法访问的
    execute(fun (_) {})
}

除了函数字面量在包含无法访问泛型类型的值形参时触发错误之外,当类型具有无法访问的泛型类型实参时也会发生错误。

例如,你在模块一中有相同的泛型类声明。在模块二中,你声明另一个泛型类:Container<C>。此外,你在模块二中声明使用 Container<C> 并以泛型类 Node<V> 作为类型实参函数

模块一模块二
kotlin
// 模块一
class Node<V>(val value: V)
kotlin
// 模块二
class Container<C>(vararg val content: C)

// 具有泛型类类型、
// 同时带有泛型类类型实参的函数
fun produce(): Container<Node<Int>> = Container(Node(42))
fun consume(arg: Container<Node<Int>>) {}

如果你尝试在模块三中调用这些函数,Kotlin 2.0.0 会触发错误,因为泛型类 Node<V> 无法从模块三访问:

kotlin
// 模块三
fun test() {
    // 在 Kotlin 2.0.0 中触发错误,因为泛型类 Node<V> 无法访问
    consume(produce())
}

在未来的版本中,我们将继续弃用一般情况下无法访问的类型。我们已经在 Kotlin 2.0.0 中通过为某些无法访问的类型场景(包括非泛型类型)添加警告来开始这一工作。

例如,让我们使用与之前示例相同的模块设置,但将泛型类 Node<V> 转换为非泛型类 IntNode,所有函数都在模块二中声明

模块一模块二
kotlin
// 模块一
class IntNode(val value: Int)
kotlin
// 模块二
// 包含 lambda
// 参数为 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>) {}

如果你在模块三中调用这些函数时,会触发一些警告:

kotlin
// 模块三
fun test() {
    // 在 Kotlin 2.0.0 中触发警告,因为类 IntNode 无法访问。

    execute {}
    // 参数 'it' 的类 'IntNode' 无法访问。

    execute { _ -> }
    execute(fun (_) {})
    // 参数 '_' 的类 'IntNode' 无法访问。

    // 在未来的 Kotlin 版本中将触发警告,因为 IntNode 无法访问。
    consume(produce())
}

现在最佳实践是什么?

如果你遇到关于无法访问的泛型类型的新警告,极有可能你的构建系统配置存在问题。我们建议检测你的构建脚本和配置。

作为最后手段,你可以为模块三配置对模块一的直接依赖项。或者,你可以修改你的代码,使类型在同一模块内可访问。

有关更多信息,请参见 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";
}

再假设有一个 Kotlin 类 Derived 继承自上述 Base 类:

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 之前,a 解析为 Derived Kotlin 类中的 Kotlin 属性,而 b 解析为 Base Java 类中的 Java 字段。

在 Kotlin 2.0.0 中,示例中的解析行为一致,确保 Kotlin 属性取代了同名 Java 字段。现在,b 解析为:Derived.b

在 Kotlin 2.0.0 之前,如果你使用 IntelliJ IDEA 跳转到 a声明或使用处,它会错误地导航到 Java 字段,而它本应导航到 Kotlin 属性。

从 Kotlin 2.0.0 开始,IntelliJ IDEA 现在正确导航到与编译器相同的位置。

一般规则是子类优先。前面的示例证明了这一点,因为 Derived 类中的 Kotlin 属性 a 被解析,因为 DerivedBase Java 类的子类。

如果继承被反转,并且 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 原语数组空安全

更改了什么?

从 Kotlin 2.0.0 开始,编译器正确推断导入到 Kotlin 的 Java 原语数组可空性。现在,它保留了与 Java 原语数组一起使用的 TYPE_USE 注解的原生可空性,并在其值未按注解使用时发出错误。

通常,当带有 @Nullable@NotNull 注解的 Java 类型从 Kotlin 调用时,它们会获得相应的原生可空性

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

然而,之前当 Java 原语数组导入到 Kotlin 时,所有 TYPE_USE 注解都会丢失,导致平台可空性和可能不安全的代码:

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 原语数组空安全现在在 Kotlin 中是标准特性,因此如果你使用它们,请检测你的代码中是否有新的警告和错误:

  • 任何在没有显式可空性检测的情况下使用 @Nullable Java 原语数组,或尝试将 null 传递给预期非非空的****原语数组的 Java 方法的代码,现在都将无法编译
  • 使用带有可空性检测@NotNull 原语数组现在会发出“不必要的安全调用”或“与 null 比较始终为 false”警告。

有关更多信息,请参见 YouTrack 中的相应问题。

expect 类中抽象成员的更严格规则

expectactual 类处于 Beta 阶段。它们几乎稳定,但未来你可能需要执行迁移步骤。我们将尽力减少你未来需要进行的任何更改。

更改了什么?

由于 K2 编译器在编译项期间分离公共和平台源代码,我们对 expect 类中的抽象成员实施了更严格的规则。

使用之前的编译器,expect 非抽象类可以继承抽象函数而无需覆盖函数。由于编译器可以同时访问公共和平台代码,编译器可以看到抽象函数actual 类中是否有相应的覆盖定义

既然公共和平台源代码是分开编译的,继承函数必须在 expect 类中显式覆盖,这样编译器才能知道该函数不是抽象的。否则,编译器会报告 ABSTRACT_MEMBER_NOT_IMPLEMENTED 错误。

例如,假设你有一个公共源代码集,你声明了一个名为 FileSystem 的抽象类,它有一个抽象函数 listFiles()。你在平台源代码集中将 listFiles() 函数****定义actual 声明的一部分。

在你的公共代码中,如果你有一个名为 PlatformFileSystemexpect 非抽象类,它继承FileSystem 类,那么 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() {}
}

现在最佳实践是什么?

如果你在 expect 非抽象类中继承抽象函数,请添加一个非抽象覆盖

有关更多信息,请参见 YouTrack 中的相应问题。

按主题领域

这些主题领域列出了不太可能影响你的代码的更改,但提供了相关 YouTrack 问题的链接,供进一步阅读。标记星号 (*) 的问题 ID 在本节开头已解释。

类型推断

问题 ID标题
KT-64189如果类型显式Normal,则属性引用编译****函数签名中的类型不正确
KT-47986禁止在构建器推断上下文中将类型变量隐式推断为上限
KT-59275K2:要求数组字面量中泛型注解调用的显式类型实参
KT-53752缺少对交集类型的子类型检测
KT-59138更改 Kotlin 中基于 Java 类型形参的类型默认表示
KT-57178更改前缀自增的推断类型为getter的返回类型,而不是 inc() 操作符的返回类型
KT-57609K2:停止依赖 @UnsafeVariance 用于逆变****形参的存在
KT-57620K2:禁止解析原始类型中的被包含成员
KT-64641K2:正确推断了带有扩展函数形参可调用引用类型
KT-57011解构变量****显式指定时,使其真实类型与显式类型一致
KT-38895K2:修复整数字面量溢出的不一致行为
KT-54862匿名类型可以从类型实参的匿名函数中暴露
KT-22379带有 breakwhile 循环条件可能产生不健全的智能类型转换
KT-62507K2:禁止在公共代码中对 expect/actual 顶层属性进行智能类型转换
KT-65750更改返回类型的自增和加操作符必须影响智能类型转换
KT-65349[LC] K2:显式指定变量类型在某些 K1 可用的情况下会破坏绑定智能类型转换

泛型

问题 ID标题
KT-54309*弃用对型变接收者使用合成setter
KT-57600禁止覆盖带有原始类型****形参的 Java 方法,而使用带有泛型类型的形参
KT-54663禁止将可能可空的类型形参传递给 in 型变的 DNN 形参
KT-54066弃用类型别名构造函数中的上限违反
KT-49404修复基于 Java 类的逆变捕获类型的类型不健全问题
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当上限是捕获类型时,报告缺失的上限违反

解析

问题 ID标题
KT-55017*重载解析期间,选择派生类中的 Kotlin 属性而非基类中的 Java 字段
KT-58260使 invoke 约定与预期解糖一致地工作
KT-62866K2:当伴生对象优先于静态作用域时,更改限定符解析行为
KT-57750在解析类型并星形导入同名类时,报告歧义错误
KT-63558K2:迁移 COMPATIBILITY_WARNING 周围的解析
KT-51194依赖类包含在同一依赖项的两个不同版本中时,CONFLICTING_INHERITED_MEMBERS 误报
KT-37592带有接收者函数类型的属性 invoke 优先于扩展****函数 invoke
KT-51666限定 this:引入/优先处理带有类型情况的限定 this
KT-54166确认类路径中 FQ 名称冲突时的未指定行为
KT-64431K2:禁止在导入中使用类型别名作为限定符
KT-56520K1/K2:类型引用在低级别存在歧义时的解析塔工作不正确

可见性

问题 ID标题
KT-64474*将无法访问类型的用法声明为未指定行为
KT-55179从内部内联函数调用私有类伴生对象成员时,PRIVATE_CLASS_MEMBER_FROM_INLINE 误报
KT-58042如果等效getter不可见,即使覆盖****声明可见,也使合成属性不可见
KT-64255禁止从另一个模块的派生类中访问内部setter
KT-33917禁止从私有内联函数中暴露匿名类型
KT-54997禁止从公共 API 内联函数中进行隐式非公共 API 访问
KT-56310智能类型转换不应影响受保护成员的可见性
KT-65494禁止从公共内联函数访问被忽略的私有操作符****函数
KT-65004K1:varsetter覆盖受保护的 val)生成为 public
KT-64972在 Kotlin/Native 的链接编译期,禁止私有成员的覆盖

注解

问题 ID标题
KT-58723如果注解没有 EXPRESSION 目标,则禁止用该注解标注语句
KT-49930REPEATED_ANNOTATION 检测期间忽略圆括号表达式
KT-57422K2:禁止在属性getter上使用以 'get' 为目标use-site注解
KT-46483禁止在 where 子句中的类型形参上使用注解
KT-64299伴生对象注解的解析会忽略伴生作用域
KT-64654K2:引入了用户和编译器所需注解之间的歧义
KT-64527枚举值上的注解不应复制到枚举值类
KT-63389K2:对包装在 ()? 中的类型的不兼容注解报告 WRONG_ANNOTATION_TARGET
KT-63388K2:对 catch 形参类型的注解报告 WRONG_ANNOTATION_TARGET

空安全

问题 ID标题
KT-54521*弃用 Java 中注解为 Nullable 的数组类型的不安全用法
KT-41034K2:更改安全调用和约定操作符组合的求值语义
KT-50850父类型顺序定义继承函数可空性****形参
KT-53982在公共签名中近似局部类型时保持可空性
KT-62998禁止将可空的****赋值给非非空的 Java 字段作为不安全赋值选择器
KT-63209报告警告级别 Java 类型的错误级别可空实参缺失的错误

Java 互操作性

问题 ID标题
KT-53061禁止源中具有相同 FQ 名称的 Java 和 Kotlin 类
KT-49882继承自 Java 集合的类根据父类型顺序具有不一致的行为
KT-66324K2:Java 类继承自 Kotlin 私有类时的未指定行为
KT-66220将 Java vararg 方法传递给内联函数运行时导致数组的数组而不是单个数组
KT-66204允许在 K-J-K 层次结构中覆盖内部成员

属性

问题 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-57871K1/K2 在不带 else 分支的圆括号条件 if 上的不一致性
KT-42995作用域****函数中带有初始化的 try/catch 代码块VAL_REASSIGNMENT 误报
KT-65724将数据流信息从 try 代码块传播到 catchfinally 代码块

枚举类

问题 ID标题
KT-57608禁止在枚举条目初始化期间访问枚举类的伴生对象
KT-34372报告枚举类中虚内联方法的缺失错误
KT-52802报告属性/字段与枚举条目之间解析的歧义
KT-47310伴生属性优先于枚举条目时,更改限定符解析行为

函数式 (SAM) 接口

问题 ID标题
KT-52628弃用无需注解便需要 OptIn 的 SAM 构造函数用法
KT-57014禁止从 lambda 返回带有错误可空性的值,用于 JDK 函数接口的 SAM 构造函数
KT-64342可调用引用形参类型的 SAM 转换导致 CCE

伴生对象

问题 ID标题
KT-54316对伴生对象成员的外部调用引用签名无效
KT-47313当 V 具有伴生对象时,更改 (V)::foo 引用解析

其他

问题 ID标题
KT-59739*K2/MPP 在公共代码中的继承者的实现位于实际对应方时报告 [ABSTRACT_MEMBER_NOT_IMPLEMENTED]
KT-49015限定 this:更改潜在标签冲突时的行为
KT-56545修复 JVM 后端中,Java 子类中意外冲突重载情况下的不正确名字修饰
KT-62019[LC 问题] 禁止在语句位置声明带有 suspend 标记的匿名函数
KT-55111OptIn:禁止在标记下进行带有默认实参(带有默认值的形参)的构造函数调用
KT-61182变量上的表达式和 invoke 解析意外允许使用 Unit 转换
KT-55199禁止将带有适配的可调用引用提升为 KFunction
KT-65776[LC] K2 破坏了 false && ... 和 `false
KT-65682[LC] 弃用 header/impl 关键字
KT-45375默认通过 invokedynamic + LambdaMetafactory 生成所有 Kotlin lambda

与 Kotlin 版本的兼容性

以下 Kotlin 版本支持新的 K2 编译器:

Kotlin 版本稳定性级别
2.0.0–2.2.10稳定
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 编译器支持:

  • Jetpack Compose 1.5.0 编译器插件及更高版本。
  • KSP2 开始支持 Kotlin 符号处理 (KSP)。

如果你使用任何其他编译器插件,请检测其文档以查看它们是否与 K2 兼容。

升级你的自定义编译器插件

自定义编译器插件使用 实验性的 插件 API。因此,API 随时可能更改,我们无法保证向后兼容性。

升级过程根据你拥有的自定义插件类型有两种路径。

仅后端编译器插件

如果你的插件只实现了 IrGenerationExtension 扩展点,则过程与任何其他新编译器版本相同。检测你使用的 API 是否有任何更改,并根据需要进行更改。

后端和前端编译器插件

如果你的插件使用了前端相关的扩展点,你需要使用新的 K2 编译器 API 重写插件。有关新 API 的简介,请参见 FIR Plugin API

如果你对升级自定义编译器插件有疑问,请加入我们的 #compiler Slack 频道,我们将尽力帮助你。

分享你对新 K2 编译器的反馈

我们感谢你的任何反馈!

  • 在我们的问题跟踪器中报告你在迁移到新 K2 编译器时遇到的任何问题。
  • 启用发送使用情况统计信息选项,以允许 JetBrains 收集关于 K2 使用情况的匿名数据。