1. 概述
本文将深入探讨 Kotlin 中集合的聚合操作(Aggregate Operations)。这些操作广泛应用于数组、列表等集合类型,通过对元素进行计算,最终返回一个汇总值。对于日常开发中处理数据统计、归约等场景非常实用,掌握它们能显著提升编码效率。
2. Kotlin 中的聚合操作
聚合操作作用于一组元素,基于集合内容计算并返回单个结果值。这类操作在数据处理、报表生成等场景中极为常见。
2.1 count()
、sum()
和 average()
- ✅
count()
:获取集合中元素的数量。 - ✅
sum()
:对所有元素求和。 - ✅
average()
:计算元素的平均值。
val numbers = listOf(1, 15, 3, 8)
val count = numbers.count()
assertEquals(4, count)
val sum = numbers.sum()
assertEquals(27, sum)
val average = numbers.average()
assertEquals(6.75, average)
⚠️ 注意:average()
返回的是 Double
类型,使用时注意精度问题。
2.2 sumBy()
与 sumByDouble()
及其替代方案 sumOf()
- ✅
sumBy()
:通过 selector 函数映射每个元素后求和,返回整型(Int)。即使原始类型是Byte
、Short
等也能使用。 - ✅
sumByDouble()
:同上,但返回Double
类型。
val sumBy = numbers.sumBy { it * 5 }
assertEquals(135, sumBy)
val sumByDouble = numbers.sumByDouble { it.toDouble() / 8 }
assertEquals(3.375, sumByDouble)
📌 踩坑提示:从 Kotlin 1.4 开始,官方推荐使用更统一的 sumOf()
替代 sumBy()
和 sumByDouble()
。它通过重载支持多种数值类型,API 更简洁。
data class Employee(val name: String, val salary: Int, val age: UInt)
val employees = listOf(Employee("Alice", 3500, 23u), Employee("Bob", 2000, 30u))
assertEquals(5500, employees.sumOf { it.salary })
assertEquals(53u, employees.sumOf { it.age })
✅ sumOf()
支持以下类型:Int
、Long
、Double
、UInt
、ULong
、BigInteger
、BigDecimal
。无需再为不同类型选择不同函数名,代码更清晰。
2.3 min()
与 max()
—— 已废弃!
- ❌
min()
和max()
:分别获取最小/最大元素。 - ⚠️ 自 Kotlin 1.4 起已标记为废弃。原因在于它们在空集合上调用会抛出异常,不符合现代 API 的安全设计。
✅ 正确做法:使用 minOrNull()
和 maxOrNull()
,明确表达可能返回 null
:
val maximum = numbers.maxOrNull()
assertEquals(15, maximum)
val minimum = numbers.minOrNull()
assertEquals(1, minimum)
📌 所有相关的 min/max
操作都应优先使用带 orNull
后缀的版本,避免运行时异常。
2.4 maxBy()
与 minBy()
—— 同样已废弃!
这两个函数根据 selector 函数的返回值来比较元素:
maxBy { selector }
:返回使 selector 结果最大的第一个元素。minBy { selector }
:返回使 selector 结果最小的第一个元素。
val maxBy = numbers.maxBy { it % 5 } // 1%5=1, 15%5=0, 3%5=3, 8%5=3 → 最大是3 → 返回3
assertEquals(3, maxBy)
val minBy = numbers.minBy { it % 5 } // 最小是0 → 返回15
assertEquals(15, minBy)
⚠️ 同样,这两个函数也已被废弃。✅ 应使用 maxByOrNull()
和 minByOrNull()
。
2.5 maxWith()
与 minWith()
—— 已废弃!
这两个函数使用 Comparator
对象来比较元素:
val strings = listOf("Berlin", "Kolkata", "Prague", "Barcelona")
val maxWith = strings.maxWith(compareBy { it.length % 4 }) // 长度模4: 6%4=2, 7%4=3, 6%4=2, 9%4=1 → 最大是3 → Kolkata
assertEquals("Kolkata", maxWith)
val minWith = strings.minWith(compareBy { it.length % 4 }) // 最小是1 → Barcelona
assertEquals("Barcelona", minWith)
✅ 正确替代方案:maxWithOrNull()
和 minWithOrNull()
。
3. Fold 与 Reduce 操作
fold
和 reduce
是函数式编程中的核心操作,用于将集合“折叠”或“归约”成单一值。理解它们的区别是避免踩坑的关键。
核心区别速查表
特性 | fold |
reduce |
---|---|---|
是否需要初始值 | ✅ 是 | ❌ 否 |
空集合行为 | 返回初始值,不报错 | ❌ 抛出 NoSuchElementException |
第一次 lambda 参数 | 初始值 + 第一个元素 | 第一个元素 + 第二个元素 |
3.1 fold()
与 foldRight()
- ✅
fold(initial) { acc, element -> ... }
:从左到右遍历,提供初始值。 - ✅
foldRight(initial) { element, acc -> ... }
:从右到左遍历,参数顺序为element
在前。
val numbers = listOf(1, 15, 3, 8)
val result = numbers.fold(100) { total, it -> total - it } // 100-1-15-3-8 = 73
assertEquals(73, result)
val resultRight = numbers.foldRight(100) { it, total -> total - it } // 100-8-3-15-1 = 73
assertEquals(73, resultRight)
3.2 foldIndexed()
与 foldRightIndexed()
当操作需要依赖元素索引时使用:
val result = numbers.foldIndexed(100) { index, total, it ->
if (index >= 2) total - it else total // 仅对索引>=2的元素做减法
}
// 计算过程: 100 (idx0, skip), 100 (idx1, skip), 100-3=97 (idx2), 97-8=89 (idx3)
assertEquals(89, result)
foldRightIndexed()
逻辑类似,但从右向左处理。
3.3 reduce()
与 reduceRight()
- ✅
reduce { acc, element -> ... }
:无初始值,使用集合首元素作为起点。 - ❌ 空集合调用会抛异常!
val result = numbers.reduce { total, it -> total - it } // 1-15-3-8 = -25
assertEquals(-25, result)
val resultRight = numbers.reduceRight { it, total -> total - it } // 8-3=5, 5-15=-10, -10-1=-11
assertEquals(-11, result)
3.4 reduceIndexed()
与 reduceRightIndexed()
带索引的 reduce 操作:
val result = numbers.reduceIndexed { index, total, it ->
if (index >= 2) total - it else total
}
// 起点: total=1 (idx0), it=15 (idx1) → 1-15 = -14
// idx2: total=-14, it=3 → -14-3 = -17 (因为 index>=2)
// idx3: total=-17, it=8 → -17-8 = -25? 不对!看原文示例结果是-10
// 原文示例逻辑可能有误,实际应为:
// idx0: total=1, it=15 → 1-15=-14
// idx1: total=-14, it=3 → -14-3=-17 (index=1<2, 应该跳过?但代码没跳)
// 此处按代码执行,结果应为 -25,但原文断言-10,疑似笔误。
// 实际使用时请以调试为准。
3.5 获取中间结果:runningFold()
与 runningReduce()
📌 Kotlin 1.4 新增功能,非常实用!可用于调试或实现滑动窗口类算法。
- ✅
runningFold()
:返回包含初始值和所有中间结果的列表。 - ✅
runningReduce()
:返回每次 reduce 操作后的结果列表。
val numbers = listOf(1, 2, 3, 4, 5)
// runningFold 示例
assertEquals(listOf(0, 1, 3, 6, 10, 15),
numbers.runningFold(0) { total, it -> total + it })
// runningReduce 示例
assertEquals(listOf(1, 3, 6, 10, 15),
numbers.runningReduce { total, it -> total + it })
这个功能在需要观察累积过程时特别有用,比如计算前缀和、动态规划状态追踪等。
4. 总结
本文系统梳理了 Kotlin 集合中的聚合操作:
- 基础统计:
count
、sum
、average
- 推荐使用
sumOf()
替代sumBy
系列 - ❗ 所有
min/max
相关函数务必使用orNull
版本 fold
vsreduce
:是否需要初始值和空集合安全性- Kotlin 1.4+ 推荐使用
runningFold
/runningReduce
获取中间状态
熟练掌握这些操作,能让集合处理代码更加简洁、安全且富有表现力。
更多 Kotlin 教程,请参考 Baeldung Kotlin Tutorials。
文中示例代码已托管至 GitHub:https://github.com/Baeldung/kotlin-tutorials/tree/master/core-kotlin-modules/core-kotlin-collections-2