编程中级 Android 开发者

Kotlin 如何实现集合和对象的不可变性(immutability),与 Java 的实践有什么不同?

用 Hintsage AI 助手通过面试

回答

问题的历史:

在 Java 中,集合的不可变性是通过 Collections.unmodifiableList() 等包装器以及手动设计 immutable 类(final 字段,无 setter)。在 Android 和服务器开发中,这种做法变得麻烦且不方便,线程安全和数据的可预测性也受到影响。

问题:

Java 集合默认情况下不安全,可以随时获取引用并修改对象。这使得控制可变性变得复杂,尤其是在多线程环境下,并导致难以发现的bug。

解决方案:

Kotlin 从一开始就设计了只读(read-only)和可变(mutable)集合的分离,通过使用 val、data class 以及没有 mutable 前缀的 List/Set/Map,使得创建真正的不可变(truly immutable)对象变得更加简单。在此基础上,通过 copy() 方法进行复制。

代码示例:

val nums: List<Int> = listOf(1, 2, 3) // 不可变列表 // nums.add(4) // 编译错误 val user = User(name = "Alex", age = 30) val updated = user.copy(age = 31) // 不可变 map 的示例 val state: Map<String, Int> = mapOf("a" to 1, "b" to 2)

关键特点:

  • 尝试修改只读集合时会导致编译错误
  • 在 Kotlin 中,val 保证引用的不可变性,但不一定是对象的不可变性
  • 为了实现真正的不可变,请仅使用只读接口并禁止修改内部状态

陷阱问题。

val 是否保证嵌套对象的绝对不可变性?

不是!val 只保证变量不能指向另一个对象,但对象的字段可以被修改(如果内部属性是 var)。

可以从只读集合中获得可变对象吗?

可以——如果集合包含可变对象,可以改变其状态,尽管集合的接口是只读的。

data class User(var name: String) val users: List<User> = listOf(User("Vasya")) users[0].name = "Petya" // 允许

通过 as? 将 List<Int> 转换为 MutableList<Int> 会发生什么?

它会编译,但如果原始类型是只读接口,将在运行时导致 ClassCastException。

常见错误和反模式

  • 将 val 视为绝对的对象不可变性保证
  • 在 "immutable" 集合中对可变对象进行引用操作
  • 将只读集合转换为可变集合

生活示例

负面案例

一个 Android 应用程序在 val List<User> 中存储状态,认为它是不可变的。开发者之一修改了 user.name。在另一个片段中,更新后的列表意外地显示不一致的数据——在发布时出现了 bug。

优点:

  • 快速处理集合

缺点:

  • 失去对变更的控制
  • 隐藏的可变性

正面案例

仅使用 val 字段的不可变数据类,过滤对嵌套对象的访问,通过 copy() 更新状态。

优点:

  • 高度可预测性和线程安全性
  • 没有意外的副作用

缺点:

  • 有时需要更多的分配和复制
  • 不是所有第三方库都提供真正的不可变对象