1. 引言
本文将深入探讨 如何在 Kotlin 中创建真正不可变的集合。
我们会先厘清“不可变”的几种类型,以及 Kotlin 原生提供的集合特性。接着,重点介绍两种实现真正不可变集合的方案:
- 使用 Google 的 Guava 库
- 使用 JetBrains 官方的 Kotlinx Immutable Collections Library(简称 KICL)
对于多线程编程、函数式风格或需要防御性编程的场景,不可变集合是必备技能。掌握这些工具能帮你有效避免数据被意外修改的“踩坑”问题。
2. 依赖配置
使用前需引入 Guava 和 KICL 的依赖。
2.1. Maven
<!-- https://mvnrepository.com/artifact/org.jetbrains.kotlinx/kotlinx-collections-immutable -->
<dependency>
<groupId>org.jetbrains.kotlinx</groupId>
<artifactId>kotlinx-collections-immutable</artifactId>
<version>0.1</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.google.guava/guava -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>27.1-jre</version>
</dependency>
<repository>
<snapshots>
<enabled>false</enabled>
</snapshots>
<id>kotlinx</id>
<name>bintray</name>
<url>https://dl.bintray.com/kotlin/kotlinx</url>
</repository>
2.2. Gradle
repositories {
maven {
url "https://dl.bintray.com/kotlin/kotlinx"
}
}
// https://mvnrepository.com/artifact/org.jetbrains.kotlinx/kotlinx-collections-immutable
compile group: 'org.jetbrains.kotlinx', name: 'kotlinx-collections-immutable', version: '0.1'
// https://mvnrepository.com/artifact/com.google.guava/guava
compile group: 'com.google.guava', name: 'guava', version: '27.1-jre'
⚠️ 注意:KICL 当时处于早期版本(0.1),生产环境使用需评估稳定性。
3. 不可变性的三种类型
集合的“不可变”并非绝对概念,通常分为三类:
✅ Mutable(可变)
集合内容可自由增删改。⚠️ Read-Only(只读)
接口层面禁止修改,但底层数据仍可能被改变。这是 Kotlin 默认的List
行为。✅✅✅ Immutable(真正不可变)
任何方式都无法修改内容,线程安全,内存更高效。
不可变集合的优势:
- 天然线程安全,可安全共享于多线程环境 ❌ 无需额外同步
- 提升代码健壮性,防止意外修改(防御性编程)
- 某些实现具备结构共享(structural sharing),提升性能
4. Kotlin 原生集合的“伪不可变”
Kotlin 的 List<T>
、Set<T>
等接口默认是 编译期只读(read-only),而非真正不可变。
虽然接口不提供 add()
等方法,但若底层是可变对象,仍可通过类型转换修改。
示例:只读 List 实际可被修改
@Test
fun givenReadOnlyList_whenCastToMutableList_checkNewElementsAdded() {
val list: List<String> = listOf("This", "Is", "Totally", "Immutable")
(list as MutableList<String>)[2] = "Not"
assertEquals(listOf("This", "Is", "Not", "Immutable"), list)
}
📌 关键点:
listOf()
返回的是一个List
接口引用- 但其实际类型可能是可变的(如
ArrayList
) - 强转为
MutableList
后即可修改 ❌ 存在隐患
这种“只读视图”容易让人误以为安全,实则暗藏风险,属于典型“踩坑点”。
5. 使用 Guava 实现真正不可变集合
Google 的 Guava 提供了真正的不可变集合实现:
ImmutableList<T>
ImmutableSet<T>
ImmutableMap<K, V>
这些集合在运行时会拒绝任何修改操作,抛出 UnsupportedOperationException
。
示例:尝试修改 ImmutableList
@Rule
@JvmField
var ee: ExpectedException = ExpectedException.none()
@Test
fun givenImmutableList_whenAddTried_checkExceptionThrown() {
val list: List<String> = ImmutableList.of("I", "am", "actually", "immutable")
ee.expect(UnsupportedOperationException::class.java)
(list as MutableList<String>).add("Oops")
}
✅ 结果:即使强转,调用 add()
也会抛出异常,确保不可变性。
5.1. 使用 copyOf
从现有集合创建
可将已有集合(即使是可变的)复制为不可变副本。
@Rule
@JvmField
var ee: ExpectedException = ExpectedException.none()
@Test
fun givenMutableList_whenCopiedAndAddTried_checkExceptionThrown(){
val mutableList: List<String> = listOf("I", "Am", "Definitely", "Immutable")
(mutableList as MutableList<String>)[2] = "100% Not"
assertEquals(listOf("I", "Am", "100% Not", "Immutable"), mutableList)
val list: List<String> = ImmutableList.copyOf(mutableList)
ee.expect(UnsupportedOperationException::class.java)
(list as MutableList<String>)[2] = "Really?"
}
📌 说明:
- 原始
mutableList
可修改 ImmutableList.copyOf()
创建的是深拷贝副本- 新集合完全独立,无法修改
5.2. 使用 Builder 构建复杂集合
Guava 提供 Builder
模式,适合逐步构建不可变集合。
@Rule
@JvmField
var ee: ExpectedException = ExpectedException.none()
@Test
fun givenImmutableSetBuilder_whenAddTried_checkExceptionThrown(){
val mutableList: List<String> = ArrayList(listOf("Hello", "Baeldung"))
val set: ImmutableSet<String> = ImmutableSet.builder<String>()
.add("I","am","immutable")
.addAll(mutableList)
.build()
assertEquals(setOf("Hello", "Baeldung", "I", "am", "immutable"), set)
ee.expect(UnsupportedOperationException::class.java)
(set as MutableSet<String>).add("Oops")
}
✅ 优势:
- 支持链式调用
- 可混合添加单个元素和整个集合
build()
后才生成不可变实例
6. 使用 Kotlinx Immutable Collections Library(KICL)
JetBrains 官方推出的 Kotlinx Immutable Collections Library 专为 Kotlin 设计,相比 Guava 更轻量(仅几百 KB),API 更符合 Kotlin 风格。
核心差异:编译期阻断 vs 运行时异常
与 Guava 不同,KICL 在编译期就阻止非法操作,而非等到运行时报错。
@Test
fun givenKICLList_whenAddTried_checkExceptionThrown(){
val list = persistentListOf("I", "am", "immutable")
// ❌ 编译失败!add() 方法根本不存在
// list.add("My new item")
assertEquals(listOf("I", "am", "immutable"), list)
}
✅ 优势:
- 类型系统保障,更安全
- 零运行时开销
- 支持持久化数据结构(persistent data structures),高效实现副本更新
📌 说明:
persistentListOf()
创建的是持久化列表- 修改操作返回新实例,旧实例不变
- 内部采用高效的树形结构,共享未变更部分
7. 总结
方案 | 不可变级别 | 安全性 | 体积 | 推荐场景 |
---|---|---|---|---|
Kotlin listOf() |
只读(Read-Only) | ⚠️ 低(可被强转修改) | ✅ 最小 | 临时只读视图 |
Guava | 真正不可变 | ✅ 高(运行时检查) | ❌ 较大(~2.6MB) | Java 项目或需丰富功能 |
Kotlinx(KICL) | 真正不可变 | ✅✅ 极高(编译期阻止) | ✅ 轻量 | Kotlin 项目首选 |
✅ 建议:
- Kotlin 项目优先考虑 KICL,类型安全且轻量
- 若已在用 Guava 或需复杂功能(如
ImmutableSortedSet
),可继续使用 Guava - 避免将
listOf()
误当作“不可变”,尤其在 API 返回或共享数据时
所有示例代码均可在 GitHub 找到。