集合操作:过滤、转换、分组与聚合¶
Kotlin 标准库给集合提供了大量扩展函数。它们覆盖搜索、过滤、转换、分组、排序、聚合、切片等常见操作。和 Java Stream 相比,Kotlin 集合操作直接挂在 Iterable、List、Set、Map 等类型上,写法更短;但默认多数操作是急切执行,会生成中间集合,性能模型不能和 Java Stream 完全等同。
本章讲通用集合操作。List、Set、Map 的专有操作见下一章。
成员函数与扩展函数¶
官方集合操作概览把集合操作分成两类:
- 成员函数:集合接口的基本能力,例如
Collection.isEmpty()、List.get()。 - 扩展函数:过滤、转换、排序、聚合等标准库能力。
自定义集合实现类时,必须实现接口要求的成员函数;而 filter()、map()、sorted() 等扩展函数通常不需要你自己实现,只要你的类型实现了对应集合接口就能使用。
Java 对比:
- Java 的
Collection、List、Map也定义成员方法。 - Java Stream 操作要先
stream()。 - Kotlin 直接
list.filter { ... }.map { ... },没有单独 Stream 类型。
操作是否修改原集合¶
通用集合操作通常返回新集合,不修改原集合:
val numbers = listOf("one", "two", "three", "four")
numbers.filter { it.length > 3 }
println(numbers) // [one, two, three, four]
如果不保存返回值,结果就丢了:
numbers.filter { it.length > 3 } // 没有实际使用结果
正确写法:
val longWords = numbers.filter { it.length > 3 }
println(longWords) // [three, four]
对可变集合,有些函数会原地修改。例如:
val mutable = mutableListOf("three", "one", "two")
val sortedCopy = mutable.sorted()
println(mutable) // [three, one, two]
println(sortedCopy) // [one, three, two]
mutable.sort()
println(mutable) // [one, three, two]
命名经验:
sorted()、reversed()、shuffled()返回新集合。sort()、reverse()、shuffle()修改可变列表本身。
To 后缀:写入目标集合¶
很多操作有 To 版本,例如 filterTo()、mapTo()、associateTo()。它们把结果写入你传入的 mutable destination:
val words = listOf("one", "two", "three", "four")
val result = mutableListOf<String>()
words.filterTo(result) { it.length > 3 }
words.filterIndexedTo(result) { index, _ -> index == 0 }
println(result) // [three, four, one]
To 版本适合:
- 避免创建额外中间集合。
- 把多个操作结果合并到同一个目标集合。
- 明确目标集合类型,例如
HashSet去重。
val lengths: HashSet<Int> = words.mapTo(HashSet()) { it.length }
println(lengths)
不要过度使用 To 版本。普通业务代码优先清晰,性能瓶颈明确后再优化分配。
Filtering¶
基本过滤:
val words = listOf("one", "two", "three", "four")
val longWords = words.filter { it.length > 3 }
println(longWords) // [three, four]
带索引过滤:
val result = words.filterIndexed { index, word ->
index != 0 && word.length < 5
}
反向条件:
val notShort = words.filterNot { it.length <= 3 }
Map 也可以 filter,lambda 参数是 Map.Entry,常用解构:
val ages = mapOf("Ada" to 36, "Bob" to 20, "Grace" to 85)
val senior = ages.filter { (name, age) ->
name.length > 3 && age > 60
}
类型与空值过滤¶
按类型过滤并缩窄类型:
val values: List<Any?> = listOf(null, 1, "two", 3.0, "four")
val strings: List<String> = values.filterIsInstance<String>()
println(strings.map { it.uppercase() })
过滤 null:
val maybeNames: List<String?> = listOf("Ada", null, "Grace")
val names: List<String> = maybeNames.filterNotNull()
Java 对比:Java Stream 中常写 filter(Objects::nonNull),但类型缩窄经常还要 map(String.class::cast)。Kotlin 的 filterIsInstance<T>() 和 filterNotNull() 会把结果类型直接变窄。
partition¶
partition() 一次把集合分成“匹配”和“不匹配”两部分:
val words = listOf("one", "two", "three", "four")
val (longWords, shortWords) = words.partition { it.length > 3 }
println(longWords) // [three, four]
println(shortWords) // [one, two]
适合需要同时处理两类结果的场景。不要写两次 filter:
val active = users.filter { it.active }
val inactive = users.filterNot { it.active }
更清晰:
val (active, inactive) = users.partition { it.active }
any、none、all¶
谓词测试:
val words = listOf("one", "two", "three", "four")
println(words.any { it.endsWith("e") }) // true
println(words.none { it.endsWith("a") }) // true
println(words.all { it.length >= 3 }) // true
不带谓词时:
println(words.any()) // 是否非空
println(words.none()) // 是否为空
注意空集合上的 all():
println(emptyList<Int>().all { it > 5 }) // true
这是逻辑中的 vacuous truth。业务上如果空集合不应该通过校验,要额外检查:
val valid = values.isNotEmpty() && values.all { it.valid }
Java 对比:Java Stream 的 allMatch() 对空流也返回 true。
map 与 mapIndexed¶
map() 把每个元素转换成新值:
val numbers = setOf(1, 2, 3)
val triple = numbers.map { it * 3 }
带索引:
val labeled = numbers.mapIndexed { index, value ->
"$index:$value"
}
过滤 null 结果:
val result = numbers.mapNotNull { value ->
if (value == 2) null else value * 3
}
Map 的 key/value 转换:
val scores = mapOf("ada" to 98, "bob" to 85)
val upperKeys = scores.mapKeys { (name, _) -> name.uppercase() }
val adjustedValues = scores.mapValues { (_, score) -> score + 1 }
注意 map() 对 Map 返回的是 List<R>,不是 Map。要保留 Map 结构,用 mapKeys() 或 mapValues()。
zip 与 unzip¶
zip() 按相同位置配对:
val colors = listOf("red", "brown", "grey")
val animals = listOf("fox", "bear", "wolf")
println(colors zip animals)
// [(red, fox), (brown, bear), (grey, wolf)]
长度不同,以较短集合为准:
println(colors.zip(listOf("fox", "bear")))
// [(red, fox), (brown, bear)]
带转换函数:
val descriptions = colors.zip(animals) { color, animal ->
"$animal is $color"
}
反向操作:
val pairs = listOf("one" to 1, "two" to 2)
val (names, values) = pairs.unzip()
Java 对比:Java 标准库没有直接 zip,需要索引循环或第三方库。
associateWith、associateBy、associate¶
associateWith():原元素做 key,lambda 生成 value:
val words = listOf("one", "two", "three")
val lengthByWord = words.associateWith { it.length }
// {one=3, two=3, three=5}
associateBy():lambda 生成 key,原元素做 value:
data class User(val id: Long, val name: String)
val users = listOf(User(1, "Ada"), User(2, "Grace"))
val userById = users.associateBy { it.id }
同时生成 key 和 value:
val nameById = users.associateBy(
keySelector = { it.id },
valueTransform = { it.name },
)
associate():lambda 返回 Pair<K, V>:
val nameLength = words.associate { word ->
word to word.length
}
官方文档提醒:associate() 会产生短生命周期 Pair 对象,性能敏感时优先使用 associateBy() / associateWith() 或 associateTo()。
重复 key 时,后面的值覆盖前面的:
val users = listOf(User(1, "Ada"), User(1, "Ada v2"))
println(users.associateBy { it.id }) // id=1 对应 Ada v2
flatten 与 flatMap¶
扁平化嵌套集合:
val nested = listOf(
setOf(1, 2, 3),
setOf(4, 5),
)
println(nested.flatten()) // [1, 2, 3, 4, 5]
先映射再扁平化:
data class Order(val items: List<String>)
val orders = listOf(
Order(listOf("book", "pen")),
Order(listOf("bag")),
)
val items = orders.flatMap { it.items }
Java Stream 对比:
orders.stream()
.flatMap(order -> order.items().stream())
.toList();
Kotlin 普通集合 flatMap 返回新 List,如果链很长或数据量很大,考虑 asSequence()。
joinToString 与 joinTo¶
生成可读字符串:
val words = listOf("one", "two", "three")
println(words.joinToString())
println(words.joinToString(separator = " | ", prefix = "[", postfix = "]"))
限制输出长度:
val numbers = (1..100).toList()
println(numbers.joinToString(limit = 10, truncated = "<...>"))
自定义元素展示:
println(words.joinToString { it.uppercase() })
写入已有 Appendable:
val builder = StringBuilder("Words: ")
words.joinTo(builder, separator = ", ")
println(builder)
不要为了日志直接打印超大集合,limit 能避免日志爆炸。
groupBy 与 groupingBy¶
groupBy() 立即构建 Map<K, List<T>>:
val words = listOf("one", "two", "three", "four", "five")
val byFirstLetter = words.groupBy { it.first() }
println(byFirstLetter)
带 value transform:
val upperByFirstLetter = words.groupBy(
keySelector = { it.first() },
valueTransform = { it.uppercase() },
)
groupingBy() 返回 Grouping,在后续操作执行时再按组处理:
val counts = words.groupingBy { it.first() }.eachCount()
按组 fold:
val totalLengthByFirst = words
.groupingBy { it.first() }
.fold(0) { total, word -> total + word.length }
选择:
- 需要每组完整列表:
groupBy()。 - 只需要计数、聚合、归约:
groupingBy()更合适,避免创建大量中间列表。
Java 对比:Java Stream 常用 Collectors.groupingBy(),计数时 Collectors.counting()。Kotlin 的 groupingBy().eachCount() 对常见场景更直接。
获取集合片段¶
slice() 按索引集合或范围取元素:
val words = listOf("one", "two", "three", "four", "five", "six")
println(words.slice(1..3))
println(words.slice(0..4 step 2))
println(words.slice(setOf(3, 5, 0)))
take() / drop():
println(words.take(3))
println(words.takeLast(3))
println(words.drop(1))
println(words.dropLast(2))
按谓词:
println(words.takeWhile { !it.startsWith("f") })
println(words.dropWhile { it.length == 3 })
takeWhile 从开头开始,遇到第一个不匹配就停止;它不是过滤整个集合。要过滤所有匹配元素用 filter()。
chunked、windowed、zipWithNext¶
按固定大小分块:
val numbers = (0..13).toList()
println(numbers.chunked(3))
直接转换每个块:
println(numbers.chunked(3) { chunk -> chunk.sum() })
滑动窗口:
val values = (1..10).toList()
println(values.windowed(size = 3, step = 2, partialWindows = true))
相邻两两配对:
val words = listOf("one", "two", "three")
println(words.zipWithNext())
zipWithNext() 不是把集合分成不重叠 pair,而是生成相邻窗口:
[1, 2, 3, 4] -> (1,2), (2,3), (3,4)
常见用途:
- 计算相邻差值。
- 检查时间序列是否递增。
- 生成滑动统计。
单元素检索¶
按位置:
val words = listOf("one", "two", "three")
println(words[0])
println(words.elementAt(1))
安全版本:
println(words.getOrNull(10))
println(words.elementAtOrNull(10))
println(words.elementAtOrElse(10) { index -> "index=$index" })
首尾:
println(words.first())
println(words.last())
按条件:
println(words.first { it.length > 3 })
println(words.firstOrNull { it.length > 10 })
println(words.find { it.startsWith("t") }) // firstOrNull 的别名
first() / last() 找不到会抛异常;不确定是否存在时优先 firstOrNull() / lastOrNull()。
firstNotNullOf¶
firstNotNullOf() 同时完成 map 和找第一个非 null:
val values: List<Any> = listOf(0, "true", false)
val firstLongText = values.firstNotNullOf { item ->
item.toString().takeIf { it.length >= 4 }
}
没有结果会抛 NoSuchElementException。安全版本:
val firstLongTextOrNull = values.firstNotNullOfOrNull { item ->
item.toString().takeIf { it.length >= 4 }
}
random 与存在性检查¶
随机元素:
val numbers = listOf(1, 2, 3)
println(numbers.random())
空集合上 random() 会抛异常,安全版本:
println(emptyList<Int>().randomOrNull())
存在性:
println("two" in words)
println(words.contains("two"))
println(words.containsAll(listOf("one", "two")))
println(words.isEmpty())
println(words.isNotEmpty())
排序¶
自然排序:
val words = listOf("one", "two", "three", "four")
println(words.sorted())
println(words.sortedDescending())
按字段排序:
data class User(val name: String, val age: Int)
val users = listOf(User("Ada", 36), User("Bob", 20))
val byAge = users.sortedBy { it.age }
val byNameDesc = users.sortedByDescending { it.name }
自定义 Comparator:
val sorted = users.sortedWith(
compareBy<User> { it.age }.thenBy { it.name }
)
Java 对比:
users.stream()
.sorted(Comparator.comparing(User::age).thenComparing(User::name))
.toList();
Kotlin 用 compareBy / thenBy 更贴近属性选择。
检查是否有序¶
官方 ordering 页面列出了一组有序性检查:
isSorted()isSortedDescending()isSortedWith(comparator)isSortedBy(selector)isSortedByDescending(selector)
示例:
val numbers = listOf(1, 2, 3, 4)
println(numbers.isSorted()) // true
val users = listOf(User("Ada", 36), User("Bob", 20))
println(users.isSortedBy(User::age)) // false
注意:
- 对没有稳定迭代顺序的集合,例如
HashSet,结果可能没有业务意义。 - 对
Sequence调用这些函数是终端操作,会消耗序列。 - 浮点排序检查中,
NaN和-0.0有特殊规则。
reversed、asReversed、shuffled¶
reversed() 返回新集合:
val reversed = words.reversed()
asReversed() 返回反向视图,更轻量,但原集合变化会反映到视图中:
val mutable = mutableListOf("one", "two", "three")
val view = mutable.asReversed()
mutable.add("four")
println(view) // [four, three, two, one]
如果不确定源集合是否会变化,优先 reversed()。
随机顺序:
val shuffled = words.shuffled()
聚合操作¶
常见聚合:
val numbers = listOf(6, 42, 10, 4)
println(numbers.count())
println(numbers.maxOrNull())
println(numbers.minOrNull())
println(numbers.average())
println(numbers.sum())
空集合上的 minOrNull() / maxOrNull() 返回 null。没有 OrNull 的版本通常会抛异常。
按 selector:
val words = listOf("one", "two", "three", "four")
println(words.maxByOrNull { it.length })
println(words.maxOfOrNull { it.length })
区别:
maxByOrNull { it.length }返回原元素。maxOfOrNull { it.length }返回 selector 的结果。
求和:
val total = users.sumOf { it.age }
JVM 上 sumOf selector 还可返回 BigInteger、BigDecimal。
fold 与 reduce¶
reduce() 用集合前两个元素开始累计:
val numbers = listOf(5, 2, 10, 4)
val sum = numbers.reduce { acc, element ->
acc + element
}
fold() 有显式初始值:
val doubledSum = numbers.fold(0) { acc, element ->
acc + element * 2
}
空集合:
reduce()会抛异常。reduceOrNull()返回 null。fold(initial)对空集合返回 initial。
右折叠:
val result = numbers.foldRight(0) { element, acc ->
acc + element * 2
}
带索引:
val evenIndexSum = numbers.foldIndexed(0) { index, acc, element ->
if (index % 2 == 0) acc + element else acc
}
保存中间结果:
println(numbers.runningReduce { acc, element -> acc + element })
println(numbers.runningFold(10) { acc, element -> acc + element })
Java 对比:Java Stream 的 reduce 经常让初学者混乱。Kotlin 中 fold 通常更安全,因为初始值和结果类型更明确。
plus 与 minus¶
集合的 + / - 返回新只读集合:
val words = listOf("one", "two", "three")
val plus = words + "four"
val minus = words - "two"
println(words) // 原集合不变
右侧可以是元素或集合:
val result = words + listOf("four", "five")
val removed = words - listOf("one", "three")
minus 右侧是单个元素时,只移除第一个匹配项;右侧是集合时,移除所有属于右侧集合的元素。
+= / -= 要看接收者:
var readOnly = listOf("one")
readOnly += "two" // 创建新 list 并重新赋值给 readOnly
val mutable = mutableListOf("one")
mutable += "two" // 修改 mutable 本身
val List<T> 不能 +=,因为它既不可变引用,又没有可变集合写能力。
Java Stream 迁移原则¶
Java:
List<String> result = names.stream()
.filter(name -> name.length() > 3)
.map(String::toUpperCase)
.sorted()
.toList();
Kotlin:
val result = names
.filter { it.length > 3 }
.map { it.uppercase() }
.sorted()
区别:
- Java Stream 是惰性流水线,终端操作触发执行。
- Kotlin 集合链默认急切,每一步通常产生中间集合。
- Kotlin
Sequence才更接近 Java Stream 的惰性模型。 - Kotlin 集合操作可读性更强,但不要无脑长链。
性能敏感时:
val result = names
.asSequence()
.filter { it.length > 3 }
.map { it.uppercase() }
.toList()
小集合、短链路优先普通集合操作。不要为了“像 Stream”总是 asSequence()。
实践建议¶
- 通用集合操作默认不修改原集合,结果必须保存或继续使用。
- 函数名带
To表示写入目标集合。 - 函数名带
OrNull表示失败时返回 null,而不是抛异常。 - 需要同时分成两类,用
partition()。 - 需要每组完整列表,用
groupBy();只要聚合,用groupingBy()。 - 排序复制用
sorted(),原地排序用sort()。 reversed()是副本,asReversed()是视图。- 空集合上慎用
reduce()、first()、single()、random()。 - 从 Java Stream 迁移时,先保持语义,再评估是否需要
Sequence。
官方参考¶
- Collection operations overview:https://kotlinlang.org/docs/collection-operations.html
- Filtering collections:https://kotlinlang.org/docs/collection-filtering.html
- Collection transformation operations:https://kotlinlang.org/docs/collection-transformations.html
- Grouping:https://kotlinlang.org/docs/collection-grouping.html
- Retrieve collection parts:https://kotlinlang.org/docs/collection-parts.html
- Retrieve single elements:https://kotlinlang.org/docs/collection-elements.html
- Ordering:https://kotlinlang.org/docs/collection-ordering.html
- Aggregate operations:https://kotlinlang.org/docs/collection-aggregate.html
- Plus and minus operators:https://kotlinlang.org/docs/collection-plus-minus.html