List、Set 与 Map 专有操作¶
Kotlin 集合 API 有很多通用操作,也有针对 List、Set、Map 的专有能力。理解这些差异,能避免把所有集合都当 Java List 或 Map 来写。
本章重点讲:
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)
keys 是 Set<K>,values 是 Collection<V>。原因是:
- key 唯一。
- value 可以重复。
对 mutable map,keys 和 values 视图上的某些删除操作可能影响原 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