集合

Kotlin 标准库提供了 ListSetMap 等集合接口,并在 API 层面区分只读集合和可变集合。对 Java 开发者来说,这一点非常重要:Kotlin 的 List<T> 是只读视图,不等于绝对不可变对象。

集合类型概览

类型 说明 可变版本
List<T> 有序,可按索引访问,允许重复元素 MutableList<T>
Set<T> 元素唯一,通常不强调顺序 MutableSet<T>
Map<K, V> 键值对,键唯一 MutableMap<K, V>

只读与可变

只读列表:

val names: List<String> = listOf("Ada", "Grace")
println(names[0])
// names.add("Linus") // 编译错误

可变列表:

val names: MutableList<String> = mutableListOf("Ada", "Grace")
names.add("Linus")

注意:val 只限制变量引用不能重新赋值,不限制对象内容:

val names = mutableListOf("Ada")
names.add("Grace") // 可以

只读不等于不可变

val mutable = mutableListOf("A", "B")
val readonly: List<String> = mutable

mutable.add("C")
println(readonly) // [A, B, C]

readonly 不能调用修改方法,但它背后的对象仍可能被其他引用修改。真正需要不可变数据结构时,要考虑复制、防御性封装或不可变集合库。

Java 对比:

  • Java 的 List 接口同时包含读取和修改方法。
  • Java 可以用 Collections.unmodifiableList()List.of() 暴露不可修改视图或集合。
  • Kotlin 在类型层面把读取接口和修改接口拆开,更利于表达 API 意图。

创建集合

val list = listOf("A", "B")
val mutableList = mutableListOf("A", "B")

val set = setOf(1, 2, 3)
val mutableSet = mutableSetOf(1, 2, 3)

val map = mapOf("one" to 1, "two" to 2)
val mutableMap = mutableMapOf("one" to 1)

to 是创建 Pair 的中缀函数,常用于 Map 字面写法。

List

val names = listOf("Ada", "Grace", "Linus")

println(names.size)
println(names[1])
println(names.first())
println(names.last())
println(names.indexOf("Grace"))

安全访问:

println(names.getOrNull(100)) // null

Java 中 list.get(100) 会抛 IndexOutOfBoundsException。Kotlin 仍然支持索引访问并可能抛异常,但标准库也提供了 getOrNull() 这类显式表达失败可能性的函数。

Set

val numbers = setOf(1, 2, 3, 3)
println(numbers) // [1, 2, 3]
println(2 in numbers)

默认 mutableSetOf() 通常使用保留插入顺序的实现,因此遍历顺序较可预测。但代码不应在没有明确约定时过度依赖 Set 的顺序。

Map

val ages = mapOf(
    "Ada" to 36,
    "Grace" to 85
)

println(ages["Ada"])
println("Grace" in ages)
println(ages.keys)
println(ages.values)

更新可变 Map:

val ages = mutableMapOf("Ada" to 36)
ages["Grace"] = 85
ages.put("Linus", 21)

map[key] 返回的是 V?,因为 key 可能不存在。这是 Kotlin 空安全在集合 API 中的体现。

常用转换操作

val names = listOf("Ada", "Grace", "Linus")

val result = names
    .filter { it.length > 3 }
    .map { it.uppercase() }
    .sorted()

println(result)

这些操作通常返回新集合,不修改原集合。和 Java Stream 相比,Kotlin 集合操作是标准库函数,语法更自然;但大量链式操作在性能敏感路径上仍要关注中间集合分配,可以考虑 asSequence()

forEach 还是 for

names.forEach { println(it) }

简单遍历可以用 forEach。如果需要 breakcontinue,传统 for 循环更直观:

for (name in names) {
    if (name.isBlank()) continue
    println(name)
}

可空元素处理

val values: List<Int?> = listOf(1, null, 2)
val nonNullValues: List<Int> = values.filterNotNull()

集合 API 经常通过返回可空值表达“没有结果”:

val firstLongName: String? = names.firstOrNull { it.length > 10 }

函数名中的 OrNull 是一个很有用的信号:调用方必须处理找不到结果的情况。

协变与可变集合

只读集合通常支持协变:

open class Shape
class Rectangle : Shape()

val rectangles: List<Rectangle> = listOf(Rectangle())
val shapes: List<Shape> = rectangles

可变集合不能这样做:

// val mutableShapes: MutableList<Shape> = mutableListOf<Rectangle>() // 不允许

原因是如果允许把 MutableList<Rectangle> 当成 MutableList<Shape>,就能往里面放入其他 Shape 子类,破坏原列表的类型安全。

实践建议

  • 对外暴露集合时优先使用只读接口 ListSetMap
  • 内部确实需要修改时使用 MutableList 等可变接口。
  • 不要误以为 List 就一定不可变。
  • 访问可能不存在的元素时,优先考虑 getOrNull()firstOrNull()singleOrNull()
  • 性能敏感的大集合链式处理可以评估 Sequence

参考

  • 官方集合概览:https://kotlinlang.org/docs/collections-overview.html