List、Set 与 Map 专有操作

Kotlin 集合 API 有很多通用操作,也有针对 ListSetMap 的专有能力。理解这些差异,能避免把所有集合都当 Java ListMap 来写。

本章重点讲:

  • List 的索引访问、搜索、二分查找、原地排序。
  • Set 的并集、交集、差集。
  • Map 的 key/value 访问、过滤、加减、写入、默认值。

List 的索引访问

List 是有序集合,可以按索引访问:

val numbers = listOf(1, 2, 3, 4)

println(numbers[0])
println(numbers.get(0))

越界会抛异常:

// numbers[5] // IndexOutOfBoundsException

安全访问:

println(numbers.getOrNull(5)) // null
println(numbers.getOrElse(5) { index -> index }) // 5

Java 对比:Java list.get(index) 越界只能抛异常。Kotlin 保留异常式索引访问,同时提供 getOrNull() / getOrElse() 表达“索引可能不存在”。

subList 是视图

subList(fromIndex, toIndex) 返回指定范围的视图:

val numbers = mutableListOf(0, 1, 2, 3, 4, 5)
val middle = numbers.subList(2, 5)

println(middle) // [2, 3, 4]

注意:它不是独立副本。原列表或 subList 的变化可能互相影响:

middle[0] = 20
println(numbers) // [0, 1, 20, 3, 4, 5]

如果你需要稳定副本:

val copy = numbers.subList(2, 5).toList()

Java 对比:Java List.subList() 也是视图,这是两边都容易踩的坑。

List 查找位置

按值查找:

val numbers = listOf(1, 2, 3, 4, 2, 5)

println(numbers.indexOf(2))     // 1
println(numbers.lastIndexOf(2)) // 4
println(numbers.indexOf(9))     // -1

按条件查找索引:

println(numbers.indexOfFirst { it > 2 })  // 2
println(numbers.indexOfLast { it % 2 == 1 }) // 5

找不到返回 -1。这一点和 Java 很像,但 Kotlin 的谓词版本更直接。

二分查找

binarySearch() 适用于已按相同规则升序排序的列表:

val words = mutableListOf("one", "two", "three", "four")
words.sort()

println(words)
println(words.binarySearch("two"))

前提非常重要:列表必须按二分查找使用的顺序排序。否则结果未定义。

找不到时,返回 (-insertionPoint - 1)

val index = words.binarySearch("zero")
if (index < 0) {
    val insertionPoint = -index - 1
    println("insert at $insertionPoint")
}

自定义比较器:

data class Product(val name: String, val price: Double)

val products = listOf(
    Product("WebStorm", 49.0),
    Product("AppCode", 99.0),
    Product("DotTrace", 129.0),
).sortedWith(compareBy<Product> { it.price }.thenBy { it.name })

val index = products.binarySearch(
    Product("AppCode", 99.0),
    compareBy<Product> { it.price }.thenBy { it.name },
)

Java 对比:Java 的 Collections.binarySearch() 也要求列表预先按同一 comparator 排序。

MutableList 写操作

按位置插入:

val words = mutableListOf("one", "five", "six")

words.add(1, "two")
words.addAll(2, listOf("three", "four"))

println(words) // [one, two, three, four, five, six]

按位置更新:

words[1] = "TWO"

填充:

val numbers = mutableListOf(1, 2, 3, 4)
numbers.fill(0)
println(numbers) // [0, 0, 0, 0]

按位置删除:

numbers.removeAt(1)

删除值:

val values = mutableListOf(1, 2, 3, 2)
values.remove(2) // 只移除第一个 2

MutableList 原地排序

返回新列表:

val words = mutableListOf("one", "two", "three")
val sorted = words.sorted()

原地排序:

words.sort()
words.sortDescending()
words.sortBy { it.length }
words.sortByDescending { it.last() }
words.sortWith(compareBy<String> { it.length }.thenBy { it })

其他原地操作:

words.shuffle()
words.reverse()

命名差异很有规律:

返回新集合 原地修改
sorted() sort()
sortedDescending() sortDescending()
sortedBy() sortBy()
shuffled() shuffle()
reversed() reverse()

asReversed 的可变视图

asReversed() 在 mutable list 上返回可变反向视图:

val words = mutableListOf("one", "two", "three")
val reversed = words.asReversed()

reversed[0] = "THREE"
println(words)    // [one, two, THREE]
println(reversed) // [THREE, two, one]

这不是副本。如果需要副本,使用:

val copy = words.reversed()

Set 专有操作

Set 的核心是集合代数。

并集:

val a = setOf("one", "two", "three")
val b = setOf("three", "four")

println(a union b) // [one, two, three, four]

交集:

println(a intersect b) // [three]

差集:

println(a subtract b) // [one, two]

对有迭代顺序的集合,并集结果会保留左操作数元素在前,再补右操作数新增元素:

println(a union setOf("four", "five"))
println(setOf("four", "five") union a)

顺序不同,输出顺序可能不同。

对 List 使用 set 操作

union()intersect()subtract() 也能作用于 List,但结果是 Set:

val x = listOf(1, 1, 2, 3, 5)
val y = listOf(1, 2, 2, 4)

println(x intersect y) // [1, 2]
println(x union y)     // [1, 2, 3, 5, 4]

重复元素会合并,结果不再支持索引语义。不要在需要保留重复次数时使用集合代数。

对称差:

val symmetricDifference = (a - b) union (b - a)

Map 获取值

按 key 获取:

val scores = mapOf("Ada" to 98, "Bob" to 85)

println(scores["Ada"])
println(scores.get("Ada"))

key 不存在时返回 null:

println(scores["Grace"]) // null

抛异常版本:

// scores.getValue("Grace") // NoSuchElementException

默认值:

println(scores.getOrDefault("Grace", 0))
println(scores.getOrElse("Grace") { 0 })

Java 对比:Java Map.get() 对“不存在”和“存在但值为 null”都返回 null。Kotlin 的 map[key] 也有这个问题,所以 nullable value 的 Map 要特别处理。

nullable Map value 的实验性 API

官方 Map 页面列出了一组实验性函数,用于区分“key 缺失”和“value 为 null”:

@OptIn(ExperimentalStdlibApi::class)
fun main() {
    val values = mapOf("one" to 1, "two" to null)

    println(values.getOrElseIfNull("two") { 0 })    // 0
    println(values.getOrElseIfMissing("two") { 0 }) // null
}

含义:

  • getOrElseIfNull():key 缺失或 value 为 null 时使用默认值。
  • getOrElseIfMissing():只有 key 缺失时使用默认值,key 存在且 value 为 null 时保留 null。

因为它们是实验性 API,公共库暴露前要谨慎;应用内部使用也要确认 Kotlin 版本和 opt-in 策略。

keys 与 values

println(scores.keys)
println(scores.values)

keysSet<K>valuesCollection<V>。原因是:

  • key 唯一。
  • value 可以重复。

对 mutable map,keysvalues 视图上的某些删除操作可能影响原 map:

val mutable = mutableMapOf("one" to 1, "two" to 2)
mutable.keys.remove("one")
println(mutable) // {two=2}

这和 Java Map 的视图集合类似。对外暴露时不要把 mutable map 的视图当作独立集合。

Map 过滤

过滤 entry:

val scores = mapOf("Ada" to 98, "Bob" to 85, "Grace" to 100)

val high = scores.filter { (name, score) ->
    name.length > 3 && score >= 90
}

只按 key:

val byKey = scores.filterKeys { it.startsWith("A") }

只按 value:

val byValue = scores.filterValues { it >= 90 }

返回的都是新 Map。

Map 的 plus / minus

+ 添加或覆盖 entry:

val scores = mapOf("Ada" to 98, "Bob" to 85)

println(scores + ("Grace" to 100))
println(scores + ("Ada" to 99)) // Ada 的值被覆盖
println(scores + mapOf("Linus" to 88, "Bob" to 90))

- 按 key 删除:

println(scores - "Ada")
println(scores - listOf("Ada", "Missing"))

注意:Map 的 - 右侧是 key,不是 value。

MutableMap 写操作

添加或更新:

val scores = mutableMapOf("Ada" to 98)

scores["Bob"] = 85
scores.put("Grace", 100)
scores += mapOf("Linus" to 88)

put() 返回旧值:

val old = scores.put("Ada", 99)
println(old)          // 98
println(scores["Ada"]) // 99

批量写入:

scores.putAll(setOf("Alan" to 91, "Barbara" to 89))

删除:

scores.remove("Ada")
scores.remove("Bob", 85) // 只有当前值匹配才删除
scores -= "Grace"

按 key/value 视图删除:

scores.keys.remove("Linus")
scores.values.remove(89) // 删除第一个匹配 value 的 entry

getOrPut 与 nullable value

getOrPut() 在 key 缺失或值为 null 时写入默认值:

val cache = mutableMapOf<String, String?>()

val value = cache.getOrPut("token") {
    loadToken()
}

如果你需要区分“缺失”和“存在但为 null”,官方提供实验性函数:

@OptIn(ExperimentalStdlibApi::class)
fun main() {
    val mapForNull = mutableMapOf<String, Int?>("one" to null)
    val mapForMissing = mutableMapOf<String, Int?>("one" to null)

    mapForNull.getOrPutIfNull("one") { 1 }
    mapForMissing.getOrPutIfMissing("one") { 1 }

    println(mapForNull)    // {one=1}
    println(mapForMissing) // {one=null}
}

缓存场景里,这个差异很关键:

  • null 表示“查过了,没有值”。
  • key 缺失表示“还没查过”。

不要用普通 getOrPut() 混淆这两种状态。

LinkedHashMap 默认顺序

Kotlin 的 mutableMapOf() 默认常用 LinkedHashMap,新 entry 通常按插入顺序迭代:

val map = mutableMapOf("one" to 1, "two" to 2)
map["three"] = 3
println(map.keys) // [one, two, three]

但 API 设计上,如果你需要顺序语义,应在文档中明确,或选择合适的具体类型。不要让调用者依赖没有说明的实现细节。

Java 迁移建议

Java 查找或写入 Map:

map.computeIfAbsent(key, k -> load(k));

Kotlin:

val value = map.getOrPut(key) { load(key) }

Java 分组:

Map<Role, List<User>> byRole = users.stream()
    .collect(Collectors.groupingBy(User::role));

Kotlin:

val byRole = users.groupBy { it.role }

Java Set 交集常要复制再 retainAll()

Set<String> result = new HashSet<>(a);
result.retainAll(b);

Kotlin:

val result = a intersect b

实践建议

  • List 越界不确定时用 getOrNull() / getOrElse()
  • subList() 是视图,稳定结果要 .toList()
  • binarySearch() 只用于已按同一规则排序的 List。
  • sort() 修改原列表,sorted() 返回新列表。
  • Set 的集合代数结果是 Set,会去重。
  • Map[key] 无法区分 key 缺失和值为 null。
  • nullable value 的 Map 缓存要明确使用 containsKey() 或实验性缺失/null 区分 API。
  • MutableMap 的 keys / values 视图可能影响原 Map。
  • 公共 API 对集合顺序有要求时,要写清楚顺序契约。

官方参考

  • List-specific operations:https://kotlinlang.org/docs/list-operations.html
  • Set-specific operations:https://kotlinlang.org/docs/set-operations.html
  • Map-specific operations:https://kotlinlang.org/docs/map-operations.html
  • Collection write operations:https://kotlinlang.org/docs/collection-write.html