Kotlin 2.4.0 现已发布,尝一尝!
大家吼哇,Kotlin 2.4.0 千呼万唤始出来哇! 老规矩,本次更新内容可以在 JetBrains 官方的 What's new in Kotlin 2.4.0 查阅, 我照例挑自己比较感兴趣的一些改动聊聊。
首先总结:这次转正的东西不少,新实验特性也挺有意思,尤其是 context parameters 稳定、集合字面量、标准库 UUID 稳定、
以及 Kotlin/JS 导出体验继续改进。
注意!这次依旧是「我个人」的更新摘要,覆盖不了全部改动;如果你对某个方向特别感兴趣,记得继续深入官方文档喔。
文中示例如无特殊说明均来自或改写自官方日志。
语言特性
一如既往,先来看语言层面的更新。这次比较醒目的有两类:一类是之前几个实验特性终于转正了,另一类是又塞进来了一些新玩具。(嗯...一如既往。)
稳定特性
首先是几个稳定下来的特性:
context parameters🔥- 属性上的
@allmeta-target - use-site annotation targets 的新默认规则
- 显式后备字段
context parameters 稳定这件事着实是特大喜事一件那!毕竟这东西从 context receivers 一路改到 context parameters,
中间变来变去也折腾了不少版本(也让我折腾了半天编译器插件),现在终于算是可以比较放心地在正式代码里试着用起来了。芜湖!
不过要注意,context parameters 的「显式 context arguments」和「callable references」还不在这次稳定范围里。
也就是说,主干能力稳定了,但边边角角还在继续打磨。
@all 和 use-site annotation targets 的默认规则稳定也挺实用,尤其是你写一些跟 Java 框架互操作的代码时,
以前各种 @field:、@param:、@get: 多个重复的内容写起来还是挺烦的。
显式后备字段也稳定了,这个我在 2.3.0 的时候就提过:
val city: StateFlow<String>
field = MutableStateFlow("")
fun updateCity(newCity: String) {
city.value = newCity
}
以前常见的 _city + city 两个属性组合,在一些场景下可以被简化掉了。好用,爱用。
显式 context arguments
书接上文。2.3.20 修改了 context parameters 的重载解析规则之后,如果两个重载只差 context 参数,
有些调用会变成歧义。现在 Kotlin 2.4.0 给出了一个新的实验性解法:显式传递 context argument。
比如官方给的例子大概是这样:
class EmailSender
class SmsSender
context(emailSender: EmailSender)
fun sendNotification() {
println("Sent email notification")
}
context(smsSender: SmsSender)
fun sendNotification() {
println("Sent SMS notification")
}
context(defaultEmailSender: EmailSender, defaultSmsSender: SmsSender)
fun notifyUser() {
sendNotification(emailSender = defaultEmailSender)
sendNotification(smsSender = defaultSmsSender)
}
这样就可以在调用点明确告诉编译器:我要的就是这个 context 参数对应的重载。
我觉得这功能属于“你平时不一定用,但一旦遇到歧义就很救命”的东西。 尤其对 DSL 或者库作者来说,这至少比通过改函数名、改参数列表来绕开歧义要优雅不少。 语法上来讲,把这东西直接作为具名参数的一员也算符合 context parameters 中的这个 parameters。
目前它还是实验性的,需要添加编译器参数:
kotlin {
compilerOptions {
freeCompilerArgs.add("-Xexplicit-context-arguments")
}
}
集合字面量
哦豁,集合字面量来了!这下代码量又得降上个 30% 了。
现在可以用方括号 [] 来创建集合:
fun main() {
val shapes: MutableList<String> = ["triangle", "square", "circle"]
println(shapes)
// [triangle, square, circle]
}
如果编译器没有足够的类型信息,它会默认推断为 List:
fun main() {
val fruit = ["apple", "banana", "cherry"]
println(fruit)
// [apple, banana, cherry]
}
这东西乍一看很像“终于补上了别的语言早就有的语法糖”,但是!Kotlin 的设计里还塞了一个更有意思的点:
可以通过 operator fun of 给自定义类型提供字面量构造能力。
比如:
class DoubleMatrix(vararg val rows: Row) {
companion object {
operator fun of(vararg rows: Row) = DoubleMatrix(*rows)
}
class Row(vararg val elements: Double) {
companion object {
operator fun of(vararg elements: Double) = Row(*elements)
}
}
}
然后就可以这样写:
fun main() {
val identityMatrix: DoubleMatrix = [
[1.0, 0.0, 0.0],
[0.0, 1.0, 0.0],
[0.0, 0.0, 1.0],
]
}
这就有点意思了。对于矩阵、向量、DSL 配置、甚至一些查询条件构造来说,应该都能玩出花来。 能有多少花活儿就...任君想象了~
不过它目前还是实验性的,而且目前不能用来构造 Java 中定义的集合类型。 启用方式如下:
kotlin {
compilerOptions {
freeCompilerArgs.add("-Xcollection-literals")
}
}
改进编译期常量
Kotlin 2.4.0 还对编译期常量做了一些实验性增强,包括:
- 支持无符号类型操作。
- 支持一些字符串标准库函数,比如
.lowercase()、.uppercase()、.trim()。 - 支持计算枚举常量和
KCallable的.name属性。
举个感觉上可能会更直观的例子:
const val NAME = " Forte ".trim().uppercase()
当然,上面这个只是为了说明用途,具体哪些函数能在编译期求值,还是要以官方支持列表为准。
它们还引入了一个 IntrinsicConstEvaluation 注解,用来标记哪些函数可以被编译期求值。
有一说一,这些东西真到需要的时候是相当有用的,尤其是无符号类型的常量和字符串常量的函数。
这个特性目前也还是实验性的:
kotlin {
compilerOptions {
freeCompilerArgs.add("-XIntrinsic-const-evaluation")
}
}
改进未使用返回值检查器
之前 2.3.0 里提过 -Xreturn-value-checker,这次又往前推了一步:
新增了一个实验性的 returnsResultOf() contract。
它的主要作用是帮助检查器分清楚:一个高阶函数返回的结果,到底是不是来自传入的 lambda。
比如:
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.contract
@OptIn(ExperimentalContracts::class)
inline fun <T, R> T.customLet(block: (T) -> R): R {
contract {
returnsResultOf(block)
}
return block(this)
}
这样检查器就可以更准确地判断下面这种情况:
fun handleNullablePackageName(packageName: String?, builder: StringBuilder) {
packageName?.customLet { builder.append(it) }
// 这里返回了一个 String,但结果没被使用,检查器可以报告 warning
packageName?.customLet { "kotlin.$it" }
}
对于写 DSL 或者工具库的人来说,这个方向其实挺重要的。Kotlin 的表达式和作用域函数太好用了, 但也确实容易写出“以为自己返回了,实际上把结果丢了”的代码。 虽然这里面有一部分也算是使用者没有仔细了解 API 的锅,但是 作为作者,多一些防备和提醒终归是好的。
启用参数如下:
kotlin {
compilerOptions {
freeCompilerArgs.add("-Xallow-returns-result-of")
}
}
@IntroducedAt
这个我觉得对库作者很有价值。
Kotlin 2.4.0 新增了实验性的 @IntroducedAt 注解,用来在给已发布 API 添加新的可选参数时,保留二进制兼容性。
过去你给一个函数加默认参数,老版本调用方可能就炸了。你要么手写隐藏的兼容 overload,
要么上 @JvmOverloads,但它又可能生成一堆你不一定需要的重载。
现在可以这么写:
@OptIn(ExperimentalVersionOverloading::class)
fun Button(
label: String = "",
color: Color = DefaultColor,
@IntroducedAt("1.1") borderColor: Color = DefaultBorderColor,
@IntroducedAt("1.2") borderStyle: Style = DefaultBorderStyle,
@IntroducedAt("1.2") borderWidth: Int = 1,
onClick: () -> Unit
) {
// Function body
}
编译器会根据这些版本信息生成对应的隐藏 overload。
这对 Compose 风格 API 或者一些公共 DSL 来说应该蛮有用。 毕竟这种 API 往往会不断加参数,而且又不想每加一次参数就让用户老版本二进制出问题。
兼容性的老大难问题一下子简化了不少,妈妈再也不怕我为了检查 ABI 兼容性而掉光头发了!
标准库
标准库这次也挺有内容的,其中我比较关注的是 UUID 稳定、排序检查,以及 nullable map 的 fallback API。
UUID API 稳定
Kotlin 2.0.20 引入的 common UUID API 终于稳定啦!
之前陆陆续续加过 UUID 解析、格式转换、和 Java UUID 互转之类的能力。到了 2.4.0, 除了生成 v4 和 v7 UUID 的那几个函数仍然是实验性的以外,其他大部分 UUID API 都稳定了。
这意味着你终于可以在公共 API 里更放心地使用 kotlin.uuid.Uuid 了。
好耶,多平台项目里又少一个自己糊类型或者引第三方库的问题。
检查是否已排序
标准库新增了一组检查集合、数组、序列是否有序的扩展函数:
.isSorted().isSortedDescending().isSortedWith(comparator).isSortedBy(selector).isSortedByDescending(selector)
例如:
data class User(val name: String, val age: Int)
fun main() {
val numbers = listOf(1, 2, 3, 4)
println(numbers.isSorted())
// true
val users = listOf(
User("Alice", 24),
User("Bob", 31),
User("Charlie", 29),
)
println(users.isSortedBy(User::age))
// false
}
一个很朴素,但是实用的 API。
无符号整数转 BigInteger
Kotlin/JVM 现在给 UInt 和 ULong 加了 .toBigInteger():
fun main() {
val unsignedLong = Long.MAX_VALUE.toULong() + 1uL
val unsignedInt = UInt.MAX_VALUE
println(unsignedLong.toBigInteger())
// 9223372036854775808
println(unsignedInt.toBigInteger())
// 4294967295
}
Kotlin: 顺手的事儿。
nullable map 的 fallback API
2.4.0 给 value 可空的 Map 增加了几组实验性的 fallback 函数,用来区分:
这个 key 是不存在,还是存在但值就是 null。
新增的函数主要有:
.getOrElseIfNull(key, defaultValue)/.getOrPutIfNull(key, defaultValue).getOrElseIfMissing(key, defaultValue)/.getOrPutIfMissing(key, defaultValue)
来看区别:
@OptIn(ExperimentalStdlibApi::class)
fun main() {
val mapForNull = mutableMapOf<String, String?>("user" to null)
val mapForMissing = mutableMapOf<String, String?>("user" to null)
mapForNull.getOrPutIfNull("user") { "default_user" }
println(mapForNull)
// {user=default_user}
mapForMissing.getOrPutIfMissing("user") { "default_user" }
println(mapForMissing)
// {user=null}
}
这对缓存场景尤其有用。比如你缓存一个查询结果,而“查到了,但是结果为空”本身也是一个有效缓存, 那你就不希望下次又重新查一遍。
@OptIn(ExperimentalStdlibApi::class)
fun getCachedResponseOrQuery(key: String): Response? =
cache.getOrPutIfMissing(key) { service.query(key) }
以前这种语义经常要自己写 containsKey 判断,现在标准库终于给了更明确的 API。
NULL safe 语言一定要对 NULL 的处理精准而全面!
Kotlin/JVM
JVM 这边这次内容不算多,但是有两个点。
支持 Java 26
Kotlin 2.4.0 开始支持生成 Java 26 字节码。
我现在主要还是稳定在用 Java21/25,对于非 LTS 版本还是涉猎比较少。
metadata 中默认写入注解
Kotlin 2.2.0 的时候,Kotlin Metadata JVM library 支持读取 metadata 中的注解。 到了 2.4.0,这个支持默认开启了。
也就是说,编译器会把注解信息写入 Kotlin metadata,注解处理器和其他工具可以在 metadata 层面读取这些内容, 而不必依赖反射或者源代码。
这对框架、代码生成、静态分析工具来说应该很有用。 我猜未来很多 Kotlin 原生工具链都会逐渐更依赖 metadata,而不是硬往 Java 反射模型里塞。
Kotlin/Native
Native 这次依旧是工程向、Apple 向、Swift 互操作向的更新。我对这块不算熟,所以主要挑几个我看得懂 (或者至少能假装看得懂) 的点。
CMS GC 默认启用
Kotlin/Native 的 concurrent mark and sweep garbage collector,也就是 CMS GC,现在默认启用了。
过去默认的 PMCS 在标记阶段需要暂停应用线程,而 CMS 可以让标记阶段和应用线程并发执行。 官方说这会显著改善 GC pause 和应用响应性,尤其对 Compose Multiplatform 这类 UI 应用有帮助。
如果遇到问题,也可以在 gradle.properties 里切回旧模式:
kotlin.native.binary.gc=pmcs
GC 也是一个需要长期演进的艰难课题哇。
Swift export 进入 Alpha
Swift export 终于进入 Alpha 了,而且这次重点改善了并发支持。
现在 Kotlin 的 suspend 函数可以更自然地导出成 Swift 的 async:
suspend fun hello(): String {
delay(1000)
return "Hello Swift! This is Kotlin."
}
Swift 侧:
let msg = try await hello()
另外,kotlinx.coroutines.flow.Flow 也可以导出成 Swift 的 AsyncSequence。
这点我觉得挺关键的,因为移动端跨平台里,异步流式数据几乎绕不开。
还记得上次对
.d.ts的导出支持suspend吗?这次 swift 也享受到了。
Swift package import
KMP 项目现在可以在 Gradle 配置里声明 Swift Package 作为 iOS app 的依赖:
kotlin {
swiftPMDependencies {
swiftPackage(
url = url("https://github.com/firebase/firebase-ios-sdk.git"),
version = from("12.11.0"),
products = listOf(
product("FirebaseAI"),
product("FirebaseAnalytics"),
)
)
}
}
如果你以前用 CocoaPods,官方也提供了迁移到 SwiftPM 的文档和工具支持。
这类更新就很明显了:KMP 在 iOS 生态里继续往“别那么别扭”的方向推进。
Apple 目标支持变化
Kotlin 2.4.0 提升了 Apple 目标的默认最低支持版本:
- iOS / tvOS 从 14.0 提升到 15.0
- macOS 从 11.0 提升到 12.0
- watchOS 从 7.0 提升到 8.0
如果你需要支持更低版本,可以通过编译器参数覆盖。
时代在前进,老设备在落泪。
我的 intel mac 还能战斗到什么时候?
Kotlin/Wasm
Wasm 这次我觉得有两个重点:增量编译默认启用,以及 WebAssembly Component Model 的实验性支持。
增量编译默认启用
Kotlin/Wasm 的增量编译从 2.1.0 开始引入,到 2.4.0 稳定并默认启用。
这意味着修改代码后,编译器只需要重编受影响的文件,构建速度会更友好一些。
如果遇到问题,可以关闭:
kotlin.incremental.wasm=false
K/Wasm 现在最大的问题之一就是开发体验还需要继续打磨,增量编译默认开肯定是好事。
支持 WebAssembly Component Model
Kotlin/Wasm 现在实验性支持 WebAssembly Component Model。
简单来说,它定义了一套用标准接口和类型组合 Wasm 组件的方式,让 Wasm 不只是一个低层二进制格式, 而是更像一个可以跨语言复用和组合的组件系统。
这也意味着 Kotlin/Wasm 不只是“跑在浏览器里”,还可以更自然地探索 serverless、FaaS 之类的场景。
官方还给了一个 wasi:http 的示例项目。这个方向我挺期待,虽然目前对大多数业务项目来说还偏早。
有机会找个小项目体验一把。
Kotlin/JS
Kotlin/JS 这次继续围绕 JavaScript / TypeScript 导出体验做改进。说实话,这几次更新下来, K/JS 的互操作正在一点点从“能用”往“像个正经 TS 生态公民”靠近。当然,能靠多近就要看 jb 有多努力了。
支持导出 value class
之前只有普通 Kotlin 类能导出到 JavaScript / TypeScript。 现在 Kotlin 2.4.0 支持导出 inline value class 了。
例如:
@JsExport
@JvmInline
value class Email(val address: String) {
init { require(address.contains("@")) { "Invalid email" } }
}
@JsExport
class AuthService {
suspend fun login(email: Email): String = TODO()
}
TS 侧会像普通类一样使用:
import { AuthService, Email } from "..."
const auth = new AuthService()
console.log(await auth.login(new Email("jane@example.com")))
value class 本来就是 Kotlin 里很适合做强类型包装的东西, 如果导出到 TS 时不能保留这种模型,就会很影响 API 设计。不赖!
inline JS 支持 ES2015
Kotlin 2.4.0 的 js() 内联代码现在完整支持 ES2015 特性,比如:
const/let- class
- generator
- arrow function
- spread / rest operator
- template string
比如:
fun spreadExample(): dynamic = js("""
const add = (a, b, c) => a + b + c;
const nums = [1, 2, 3];
const sum = add(...nums);
return { sum };
""")
这个对需要跟第三方 JS 库做细颗粒度互操作的人来说,还是挺舒服的。 毕竟 2026 年了,在内联 JS 里还不能好好写现代 JS 确实有点说不过去。
不过我本人在 js("...") 里写大段 JS 片段的情况还算比较少,所以之前也没怎么体会到问题所在。
导出 TypeScript 时保留型变
以前 Kotlin 泛型里的 in / out 信息导出到 TypeScript 时会丢掉。
现在它会被保留并映射到 TypeScript 的 variance annotations。
例如 Kotlin:
interface Producer<out T> {
fun produce(): T
}
interface Consumer<in T> {
fun consume(item: T)
}
生成的 .d.ts 会保留:
export interface Producer<out T> {
produce(): T;
}
export interface Consumer<in T> {
consume(item: T): void;
}
类型系统信息少丢一点,互操作就少一点坑。
改进接口导出
新的 @JsNoRuntime 注解可以让 Kotlin 接口导出 成更普通的 TypeScript interface,
不再携带之前那些为了 Kotlin runtime 识别接口所需的 metadata。
例如:
import kotlin.js.JsNoRuntime
@JsNoRuntime
expect interface DataProcessor {
fun process(data: String): Int
}
生成出来就是:
export interface DataProcessor {
process(data: string): void;
}
当然代价也很明确:标记了 @JsNoRuntime 之后,就不能再对这个接口做 is / as 检查、::class 引用,
也不能作为 reified 类型参数传来传去。
这就是一个取舍:如果你想要更像 TypeScript 原生接口,那就别指望它还保留 Kotlin runtime 的那些能力。
另外,这次还放宽了 @JsExport 导出接口的限制,现在可以导出带嵌套类和命名伴生对象的接口了。
Gradle & Maven
构建工具这块简单看一下。什么,还有 Maven 的事儿?
Gradle 方面,Kotlin 2.4.0 兼容 Gradle 7.6.3 到 9.5.0,最低支持的 Android Gradle Plugin 提升到了 8.5.2。
另外,默认 module name 在各平台之间统一为 {group}:{project_name},减少跨平台命名不一致带来的冲突。
如果你想回到旧的 JVM module name,可以手动配置:
kotlin {
compilerOptions.moduleName(project.name)
}
Maven 方面,这次做了两个比较实用的改进:
- Kotlin Maven 插件可以自动对齐 Java compiler version 和 Kotlin JVM target。
- 支持 Maven Toolchains,用来统一控制 Kotlin 编译使用的 JDK。
虽然我个人还是选择 Gradle,但 Maven 用户看到这个应该会舒服不少。
编译器、插件和 Compose
klib 编译时同模块 inline 行为更一致
Kotlin/Native、Kotlin/JS、Kotlin/Wasm 现在在生成 .klib 时,会默认对同模块内的 inline 函数进行 inline。
这一步是在向 JVM 的行为靠拢,让不同平台对 inline 的兼容性保证更一致。 如果遇到问题,可以用:
-Xklib-ir-inliner=disabled
未来还计划继续推进跨模块 inline。
OS: 什么,原来之前不会 inline 的吗?
kapt 可以排除 compile classpath 上的注解处理器
kapt 现在支持 includeCompileClasspath 配置,可以排除不必要的 annotation processor discovery。
Maven 里可以这样配:
<properties>
<kapt.include.compile.classpath>false</kapt.include.compile.classpath>
</properties>
Power-assert 新 runtime library
Power-assert 这次引入了新的 runtime library。
新的 @PowerAssert 注解可以让断言函数更容易被编译器插件发现,CallExplanation 也能提供更详细的调用点信息。
如果你喜欢写测试,或者想给自己的断言库接入 Power-assert,这个方向值得关注。但是我用的不多,就不过多评价了。
Compose 编译器
Compose compiler 这次主要是让 internal declaration 的增量编译更一致。
不过副作用是:当 @Composable 函数使用来自其他文件的 internal 类型作为参数时,产物体积可能会变大。
官方说 R8 这类全程序优化工具可以消掉这部分额外开销。
此外,一些已经稳定或准备淘汰的 feature flag 也进入新的弃用阶段:
StrongSkipping、IntrinsicRemember相关 DSL 提升到DeprecationLevel.ERROR,计划 2.5.0 移除。OptimizeNonSkippingGroups、PausableComposition已弃用,计划 2.6.0 移除。
破坏性变更和弃用
这次有几个需要注意的兼容性点:
- Kotlin 2.4.0 不再支持
-language-version=1.9,也就是说 K1 编译器正式不再支持。(我的编译器插件终于不用再继续兼容旧编译器了😭) - Kotlin Gradle Plugin 的 binary compatibility validation DSL 做了精简并弃用了一些旧配置。
- Maven 插件里的
KotlinScriptMojo脚本执行支持被移除了。
如果你的项目还卡在比较老的语言版本,或者构建脚本里有一堆祖传 Kotlin 配置,这次升级前最好扫一眼官方 compatibility guide。
其他
官方这次还更新了一堆文档,比如 KMP、SwiftPM 迁移、Compose Multiplatform、Ktor、KSP、Lincheck、Kotlin LSP 等内容。 如果你最近正好在折腾这些生态工具,不妨顺手去翻翻。 文档,多多益善!
尾声
这次 Kotlin 2.4.0 给我的感觉是:语言层面继续补语法糖和工具能力,多平台方向继续推进工程化和互操作体验。
context parameters 稳定和集合字面量我是已经期待了很久了,这次也是相当满足。
而那个用于兼容性的 @IntroducedAt 则是一个意外之喜,真是令我欢喜!这个要是能支持到 data class 上就更好了,抽空得狠狠体验一把了。
你呢?这次 2.4.0 里有没有哪个新玩具让你眼前一亮?