1. 简介

本文将介绍从旧版 Kotlin 编译器(K1)迁移到新版 K2 编译器的完整流程。我们只聚焦于迁移本身,不涉及 K2 的性能优化或新语言特性。后文提到的“K2”即指新编译器,“K1”为旧编译器——这也是社区中广泛使用的称呼。

2. 迁移流程概述

K2 编译器并非完全兼容 K1,直接切换可能会导致编译失败。你需要对现有代码进行一些调整才能顺利通过 K2 编译。官方已提供详细的 K2 迁移指南,本文则提炼出最可能影响普通开发者的关键变更点,帮你快速避坑✅。

3. Open 属性必须立即初始化

⚠️ 核心变化:K2 要求所有带 backing field 的 open valopen var 属性都必须在声明时或构造函数中立即初始化。而 K1 仅强制要求 open var 初始化,open val 可延迟到 init 块中赋值。

这意味着以下代码在 K2 中将无法通过编译:

open class BaseEntity {
    open val points: Int
    open var pages: Long?

    init {
        points = 1
        pages = 12
    }
}

虽然这段代码和下面的写法在字节码层面几乎等价:

open class BaseEntity {
    open val points: Int = 1
    open var pages: Long? = 12 
}

但 K2 更严格地执行了语义一致性原则——既然 open var 要求立即初始化以防止子类覆盖前访问未定义状态,那 open val 同样应受此约束。

例外情况lateinit open var 仍允许延迟初始化:

open class WithLateinit {
    open lateinit var point: Instant
}

上述代码在 K2 下可正常编译。

💡 踩坑提示:如果你的基类中有大量 open valinit 中赋值,迁移时需逐一改为构造函数参数或直接初始化。

4. 投影类型上的合成 setter 限制

这一改动修复了一个长期存在的类型安全漏洞,理解它需要先回顾泛型投影的基本原理。

4.1. 泛型类型的约束机制

考虑如下 Java 代码:

public void add(List<?> list, Object element) {
    list.add(element);
}

该代码无法编译。原因很明确:List<?> 可能指向 List<Number>List<List<Object>> 等任意具体类型,向其中添加任意对象会破坏类型安全。因此,Java 仅允许向通配符集合中添加 null

Kotlin 中的星投影(*)具有类似行为:

fun execute(list: MutableList<*>, element: Any) {
    list.add(element) // ❌ 编译错误
}

两者逻辑一致:未知类型的容器不允许写入非 null 值。

4.2. K1 中的合成 setter 安全漏洞

假设我们有如下 Java 类:

public class Box<E> {
    private E value;

    public E getValue() {
        return value;
    }

    public void setValue(E value) {
        this.value = value;
    }
}

在 Kotlin 中使用时,显式调用 setValue() 是类型安全的:

fun explicitSetter() {
    val box = Box<String>()
    val tmpBox: Box<*> = box
    tmpBox.setValue(12) // ❌ 编译错误!安全拦截
    val myValue: String? = box.value
}

但 K1 存在一个严重缺陷:通过属性语法(即合成 setter)绕过检查竟可成功编译:

fun syntheticSetter() {
    val box = Box<String>()
    val tmpBox: Box<*> = box
    tmpBox.value = 12 // ✅ K1 竟然允许!
    val foo: String? = box.value // 💥 运行时 ClassCastException
}

问题根源在于:

  • tmpBoxBox<*> 类型,实际应禁止写入;
  • K1 错误地生成了对 value 字段的直接赋值指令;
  • 当后续通过 box.value 读取时,Kotlin 自动生成的 getter 会插入 (String) 强制转换,导致运行时崩溃。

K2 已修复此问题:无论是星投影 Box<*> 还是逆变投影 Box<in String>,都不再允许通过合成 setter 写入:

fun syntheticSetter_inVariance() {
    val box = Box<String>()
    val tmpBox: Box<in String> = box
    tmpBox.value = 12 // ❌ K2 编译失败
    val foo: String? = box.value
}

⚠️ 影响范围:所有涉及 Java 泛型类 + Kotlin 投影类型 + 属性赋值的场景均需检查。

5. 属性解析顺序一致性

当 Kotlin 类继承 Java 类且存在同名字段时,K1 在属性解析上存在歧义,可能导致运行时异常。

5.1. 问题复现

Java 基类:

public class AbstractEntity {
    public String type = "ABSTRACT_TYPE";
    public String status = "ABSTRACT_STATUS";
}

Kotlin 子类:

class AbstractEntitySubclass(val type: String) : AbstractEntity() {
    val status: String
        get() = "CONCRETE_STATUS"
}

fun main() {
    val subclass = AbstractEntitySubclass("CONCRETE_TYPE")
    println(subclass.type)
    println(subclass.status)
}

K1 行为:

  • 编译通过;
  • 运行时报 java.lang.IllegalAccessError

原因剖析:

  • 子类 type 是私有字段(由主构造函数生成),需通过 getType() 访问;
  • K1 错误地生成了直接访问子类私有字段的 getfield 指令;
  • JVM 拒绝跨类访问私有成员,抛出非法访问异常。

5.2. K2 的解决方案

K2 明确了属性解析优先级规则:

子类属性优先原则:在继承链中,更具体的子类属性始终优先,且必须通过合法访问方式(如 getter)调用。

因此,K2 编译上述代码输出为:

CONCRETE_TYPE
CONCRETE_STATUS

完全符合预期,无运行时风险。

💡 设计哲学:K2 更倾向于“显式优于隐式”,避免依赖模糊的字段解析策略。

6. 原生类型数组的可空性保留

Kotlin 编译器支持识别 Java 中的 @Nullable / @NotNull 注解,用于推断可空类型。但 K1 在处理原生类型数组(如 char[])时存在缺陷。

6.1. K1 的缺陷

Java 方法返回可空字符数组:

public static char @Nullable [] toCharArray(String s) {
    if (s == null) return null;
    return s.toCharArray();
}

K1 无法正确解析该注解,导致 Kotlin 侧误判返回类型为非空 CharArray

val array: CharArray = toCharArray(null) // ✅ K1 竟然允许!
println(array[0]) // 💥 NPE

这极易引发空指针异常。

6.2. K2 的改进

✅ K2 正确识别类型使用位置的可空注解,将 toCharArray() 推断为返回 CharArray?

val array: CharArray = toCharArray(null) // ❌ 编译失败
val array: CharArray? = toCharArray(null) // ✅ 正确写法

此举显著提升了 Java/Kotlin 混合项目中的空安全边界。

7. 总结

K2 编译器在类型安全、互操作性和语义一致性方面带来了实质性提升:

  • ✅ 强制 open val 初始化,杜绝状态不一致;
  • ✅ 修复投影类型上的合成 setter 安全漏洞;
  • ✅ 明确继承链中属性解析顺序,消除运行时异常;
  • ✅ 正确处理原生数组的可空性注解,减少 NPE 风险。

尽管迁移过程可能需要修改部分代码,但从长远看,这些变更有助于构建更健壮、更可维护的 Kotlin 应用。

文中示例代码可在 GitHub 获取:Baeldung/kotlin-tutorials/k2-compiler


原始标题:K2 Compiler Migration Guide