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)。即使原始类型是 ByteShort 等也能使用。
  • 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() 支持以下类型:IntLongDoubleUIntULongBigIntegerBigDecimal。无需再为不同类型选择不同函数名,代码更清晰。

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 操作

foldreduce 是函数式编程中的核心操作,用于将集合“折叠”或“归约”成单一值。理解它们的区别是避免踩坑的关键。

核心区别速查表

特性 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 集合中的聚合操作:

  • 基础统计:countsumaverage
  • 推荐使用 sumOf() 替代 sumBy 系列
  • ❗ 所有 min/max 相关函数务必使用 orNull 版本
  • fold vs reduce:是否需要初始值和空集合安全性
  • 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


原始标题:Aggregate Operations in Kotlin