9 年前的伏笔
2017 年 6 月,JetBrains 公布过一份 Kotlin 未来特性的调查结果。
那次调查大约收到了 850 份回复,排在前列的诉求包括:集合字面量、Kotlin 接口的 SAM 转换,以及真正不可变的数据。
JetBrains 当时说,这些反馈会影响后续规划,但并没有承诺具体什么时候交付。
终于进入 EAP
集合字面量(Collection Literals)在 YouTrack 里以 KT-43871 的形式沉寂多年后,终于进入了 Kotlin——更准确地说,是以 EAP 的形式进入了 Kotlin。
在 2026 年 4 月 22 日发布的 2.4.0-Beta2 中,方括号集合字面量作为实验性特性被引入 Kotlin。
如果你也曾经无数次写下 mutableListOf<String>()、arrayOf<Int>(),大概率会对这个特性会心一笑。它确实能减少一些重复的样板代码。
对我来说,我一直不太习惯"创建数组或列表必须调用函数"这件事。类似 C 语言那样的 [] 写法,会让我更直观地意识到:这里就是一个集合。读代码时,它也能更快地传达结构信息。
不过,这个特性的设计并不只是"把 listOf 换成 []"这么简单。先别急着下结论,如果你以为 [] 只是数组语法,那就有点低估 Kotlin 了。
读完 KEEP-0416 之后,你可能也会冒出同样的想法:"等等,它做的事情比我想象中多得多。"
这个特性会影响编译器选择哪个方法,同时也附带了一长串限制。这些限制也能看出,Kotlin 团队从 Swift、C# 12,以及被拒绝的 Java JEP 中吸收了不少经验。
废话少说,开干。
五秒钟看懂它
// Before
val supportedLocales: List<String> = listOf("en", "es", "fr", "de", "ja")
// After (Kotlin 2.4.0-Beta2, with -Xcollection-literals)
val supportedLocales: List<String> = ["en", "es", "fr", "de", "ja"]
生成的字节码大体相同,但代码里不再需要显式写 listOf 这类构建函数,至少少敲了很多字符。
编译器会把方括号转换为对目标类型伴生对象上 operator fun of 方法的调用。在这个例子里,也就是 List.Companion.of("en", "es", ...)。
同时,标准库也已经更新,为最常见的集合类型提供了对应的 of 操作符。
如果你没有明确告诉编译器想要什么类型,它会默认回退到 List:
val tags = ["beta", "internal", "qa"] // List<String>
这个回退规则并不是一个随手加上的小设计。它有意呼应了 Swift 处理数组字面量的方式,也有意区别于 C# 12:在 C# 12 中,集合表达式需要显式目标类型。
Kotlin 团队选择了一条对新手更友好的路线。
并非 List 专属
这些 [] 并不是"列表字面量"。它们会根据你期望的类型自动变化。
MutableList、Set、MutableSet 都可以使用,因为标准库的伴生对象已经实现了对应的 operator fun of 重载:
val experimentalFeatures: Set<String> = [
"关注 RockByte 公众号",
"关注 RockByte 掘金号",
"关注 RockByte 公众号",
]
println(experimentalFeatures)
结果也符合 Set 的语义:
[关注 RockByte 公众号, 关注 RockByte 掘金号]
不过,目前有一件事还不能做:不能把目标类型设成 Java 定义的集合类型。这个限制很容易踩坑,值得提前标出来:
import java.util.ArrayList
val items: ArrayList<String> = ["a", "b"] // Won't compile
根据 KT-80494,Java 定义的集合目前还不受支持。
在 Android 上,androidx.collection.ArraySet 这类类型经常出现在性能敏感的场景中,这会让迁移稍微麻烦一些。修复思路本身很直接:声明一个满足操作符限制的静态 of 方法。
但真正的问题在于,你不能修改第三方 Java 代码(除非你是它的主人)。
此外,你也不能把目标类型设成 MutableCollection、MutableIterable 这类抽象或写入受限的类型,也不能设成交叉类型。
前者是因为编译器没有为它们提供 Companion.of;后者则是设计上明确禁止的,以免编译器选择方法时变得含糊不清。
自定义容器也能用方括号
这个机制是开放的,任何类型只要在伴生对象上提供格式正确的 operator fun of,就可以使用方括号。
下面是一个 NonEmptyList<T> 的例子:
class NonEmptyList<T> private constructor(
val head: T,
val tail: List<T>,
) {
val size: Int get() = 1 + tail.size
fun toList(): List<T> = listOf(head) + tail
companion object {
operator fun <T> of(first: T): NonEmptyList<T> =
NonEmptyList(first, emptyList())
operator fun <T> of(first: T, vararg rest: T): NonEmptyList<T> =
NonEmptyList(first, rest.toList())
}
}
val onboarding: NonEmptyList<OnboardingStep> = [
OnboardingStep.Welcome,
OnboardingStep.PermissionsRequest,
OnboardingStep.AccountLinking,
]
这里有两点值得注意。
- 这两个
of重载是合法的,因为它们只在参数数量上不同,而且vararg rest前面的每个参数都和rest本身拥有相同类型。这样写,能强迫你必须创建一个非空的容器,因为你不能[]这么写,这是个非常值得学习的小技巧。 - 两个重载必须返回相同类型,并且拥有相同可见性。
递归
当期望类型本身也支持方括号时,你可以嵌套使用:
class WeeklySchedule(val days: List<DayPlan>) {
companion object {
operator fun of(vararg days: DayPlan) = WeeklySchedule(days.toList())
}
}
class DayPlan(val activities: List<String>) {
companion object {
operator fun of(vararg activities: String) = DayPlan(activities.toList())
}
}
val schedule: WeeklySchedule = [
["yoga", "code review", "deploy"],
["standup", "1:1s"],
["focus block", "ship the PR"],
]
编译器会一层一层解析这段代码,并在每一层使用该层的期望类型来选择正确的 of 重载。
这件事值得我们仔细推敲一遍:外层方括号会根据 WeeklySchedule.Companion.of 解析,而它需要的是 vararg DayPlan,于是内层方括号又会根据 DayPlan.Companion.of 解析。
这里没有魔法,只是类型系统在做它本来就擅长的事。
但在矩阵式或网格式数据中,这种写法带来的可读性提升是真实存在的。这也是 KEEP 把数学矩阵记法列为语法灵感的原因。
不太显眼的性能设计
不仅是语法和语言层面上的改动,很意外的是,它还包含了一个针对常见成员检查模式的 contains 优化:
if (countryCode in ["US", "CA", "MX"]) {
enableNorthAmericaFeatures()
}
按照 KEEP 的设计意图,这类写法可以被编译器优化成类似下面这样的代码:
val tmp = countryCode
if (tmp == "US" || tmp == "CA" || tmp == "MX") {
enableNorthAmericaFeatures()
}
没有集合分配,没有迭代器,只有一串 OR 判断。
如果你曾经分析过热点路径,并亲眼看着一个微小分配在高负载下被不断放大,就会理解这个设计的用心。
IDE 预计也会把这种位置上的重复元素作为单独的检查项标出来。
能优化到这个程度,说明 KEEP 中确实想过实际场景里的性能问题,而不只是在设计语法。
kotlin.collections.List
标准库会原生支持伴生对象级工厂方法,概念上类似这样:
public expect interface List<out E> : Collection<E> {
// ... existing members ...
public companion object {
public operator fun <T> of(vararg elements: T): List<T>
public operator fun <T> of(element: T): List<T>
public operator fun <T> of(): List<T>
}
}
如果你平时用的是 kotlin.collections.List,有几件事需要提前说明:
-
kotlin.collections.List是一个映射类型,也就是说,它在 JVM 运行时对应的是java.util.List。不过,List.Companion.of属于 Kotlin 标准库提供的伴生对象工厂,并不是java.util.List本身的方法。这一点也解释了前面提到的限制:Java 定义的集合类型不会自动获得这套方括号构造能力。 -
kotlin.collections.List.of(null)的语义和listOf(null)对齐:它返回List<Nothing?>,而不是抛出异常。这对一个 Kotlin 风格的 API 来说是合理的选择,但它和java.util.List.of(null)的行为不同,后者会抛出 NPE。
提一下,现在 Java 确实有of方法:// Java 的 of 源码 static <E> List<E> of() { return (List<E>) ImmutableCollections.EMPTY_LIST; }Set也是类似的:Set.of(1, 1)会去重,而不是抛出IllegalArgumentException。这匹配的是setOf,不是java.util.Set.of。 -
listOf和它的那些类似函数不会被废弃。它们使用太广泛了,第三方库也普遍遵循smthOf这类命名约定,而且listOfNotNull无论如何都没有方括号等价写法。
所以在很长一段时间里,你都会在同一个代码库中同时看到两种风格:既有 [],也有 listOf。
当然,目前还有三件事明确不在范围内:
- Map 字面量:
["key": 1]目前还不存在。KEEP 给出了三个原因:Java 互操作痛点、装箱性能,以及Map.Entry是接口带来的尴尬。语法空间会先保留,也许未来再交付。 - Tuple 字面量:
Pair和Triple不能通过方括号触达,部分原因是operator fun of的形状无法在单个vararg中表达异构类型。 - 没有期望类型的空字面量:
val x = []会编译失败,原因和emptyList()返回List<T>而不是List<Nothing>类似。
对今天意味着什么
老实说,对生产环境来说,短期内影响还不大,这个特性仍然受实验性编译器标志保护:
// build.gradle.kts
kotlin {
compilerOptions {
freeCompilerArgs.add("-Xcollection-literals")
}
}
如果这个特性未来稳定发布,在我看来,真正的收益应该不仅仅是少打了几个字符。
更重要的是,你自定义的集合类型也可以获得接近标准库集合的构造体验。很多开发者和开源库都会为了业务语义定义自己的集合类型,但这些类型的构造语法往往比 listOf 难看得多。
NonEmptyList、RouteTable、PermissionSet 这些类型,终于可以在观感和使用体验上与标准库集合保持一致了。以后你写自定义集合类型时,也可以写出 val routes: RouteTable = [routeA, routeB, routeC] 这样的代码。
结论
从 Kotlin 2.4.0-Beta2 开始,集合字面量以实验性特性的形式进入 Kotlin;截至 2.4.0-RC,官方文档中它仍然是实验性支持。它的核心规则是:方括号会转换成对目标类型伴生对象上 operator fun of 的调用;如果没有可用的期望类型,则回退到 List。
它是多态的、递归的、由类型驱动的,同时也受到十条精心设计的限制约束,并且针对 x in [...] 这类成员检查,编译期有计划进行优化。
它不是 Kotlin 2.x 里最重量级的特性,上下文参数和显式 backing field 大概更有资格拿这个头衔。但从 2017 年调查到现在快十年了,它总算到落地的阶段了。
这也正是我喜爱 Kotlin 的原因:它不会无视开发者的反馈。