集合操作:过滤、转换、分组与聚合

Kotlin 标准库给集合提供了大量扩展函数。它们覆盖搜索、过滤、转换、分组、排序、聚合、切片等常见操作。和 Java Stream 相比,Kotlin 集合操作直接挂在 IterableListSetMap 等类型上,写法更短;但默认多数操作是急切执行,会生成中间集合,性能模型不能和 Java Stream 完全等同。

本章讲通用集合操作。List、Set、Map 的专有操作见下一章。

成员函数与扩展函数

官方集合操作概览把集合操作分成两类:

  • 成员函数:集合接口的基本能力,例如 Collection.isEmpty()List.get()
  • 扩展函数:过滤、转换、排序、聚合等标准库能力。

自定义集合实现类时,必须实现接口要求的成员函数;而 filter()map()sorted() 等扩展函数通常不需要你自己实现,只要你的类型实现了对应集合接口就能使用。

Java 对比:

  • Java 的 CollectionListMap 也定义成员方法。
  • 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 还可返回 BigIntegerBigDecimal

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