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. 不可变性的三种类型

集合的“不可变”并非绝对概念,通常分为三类:

  1. Mutable(可变)
    集合内容可自由增删改。

  2. ⚠️ Read-Only(只读)
    接口层面禁止修改,但底层数据仍可能被改变。这是 Kotlin 默认的 List 行为。

  3. ✅✅✅ 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 找到。


原始标题:Kotlin Immutable Collections