1. 简介

假设我们有一个列表,例如 listOf(a, b, c, d, e, f),我们希望将其拆分成多个子列表,例如:listOf(listOf(a, b), listOf(c, d), listOf(e, f)) 或者 listOf(listOf(a, b, c), listOf(d), listOf(e, f))

在本教程中,我们将使用 Kotlin 中的 groupBychunkedwindowed 方法实现这一功能,并比较它们之间的差异。

2. 将列表拆分为成对的子列表

我们使用两个列表作为示例:一个元素数量为偶数,一个为奇数:

val evenList = listOf(0, "a", 1, "b", 2, "c")
val unevenList = listOf(0, "a", 1, "b", 2, "c", 3)

我们希望将 evenList 拆分为三个包含两个元素的子列表,而 unevenList 则会多出一个单独元素。

2.1. 使用 groupBy

我们可以使用 groupBy 实现列表拆分。例如:

val numberList = listOf(1, 2, 3, 4, 5, 6)
numberList.groupBy { (it + 1) / 2 }.values

输出结果为:

[[1, 2], [3, 4], [5, 6]]

原理是:groupBy 会为每个元素计算一个分组键,相同键的元素会被归为一组。

但如果我们使用一个不连续的列表,例如:

val numberList = listOf(1, 3, 8, 20, 23, 30)
numberList.groupBy { (it + 1) / 2 }.values

结果会是:

[[1], [3], [8], [20], [23], [30]]

因为每个元素的分组键都不相同,所以无法形成有效分组。而且如果元素类型不是数字,这段代码甚至无法编译。

2.2. 使用 withIndex() + groupBy

更通用的方法是基于索引分组:

evenList.withIndex()
    .groupBy { it.index / 2 }
    .map { it.value.map { it.value } }

输出结果为:

[[0, "a"], [1, "b"], [2, "c"]]

对于 unevenList,也能正确处理最后一个单独元素:

[[0, "a"], [1, "b"], [2, "c"], [3]]

2.3. 使用 foldIndexed

我们也可以使用 foldIndexed 实现更高效的拆分逻辑:

evenList.foldIndexed(ArrayList<ArrayList<Any>>(evenList.size / 2)) { index, acc, item ->
    if (index % 2 == 0) {
        acc.add(ArrayList(2))
    }
    acc.last().add(item)
    acc
}

虽然代码更长,但这种方式避免了创建额外的索引包装对象,性能更优。

2.4. 使用 chunked

Kotlin 标准库提供了更简洁的方法:chunked,它直接按固定大小拆分列表:

evenList.chunked(2)

输出:

[[0, "a"], [1, "b"], [2, "c"]]

对于 unevenList,也能正确包含最后一个元素:

[[0, "a"], [1, "b"], [2, "c"], [3]]

✅ 优点:简洁、直观、无需手动处理索引逻辑。

2.5. 使用 windowed

windowed 提供了更灵活的窗口滑动功能,适用于需要更细粒度控制的场景:

evenList.windowed(2, 2)

默认不包含部分窗口,输出:

[[0, "a"], [1, "b"], [2, "c"]]

若希望包含部分窗口,可设置 partialWindows = true

unevenList.windowed(2, 2, true)

输出:

[[0, "a"], [1, "b"], [2, "c"], [3]]

⚠️ 踩坑提醒:windowed 的参数顺序是 size, step, partialWindows,容易搞错顺序。

3. 关于 partition() 方法

除了将列表拆分成固定大小的子列表,有时我们还需要根据某个条件将列表划分为两个部分。这时可以使用 partition()

val numbers = listOf(42, 1984, 1, 0, -4, 23, 100, 6, 8)
val (lessThan42, greaterThanOrEq42) = numbers.partition { it < 42 }

assertEquals(listOf(1, 0, -4, 23, 6, 8), lessThan42)
assertEquals(listOf(42, 1984, 100), greaterThanOrEq42)

✅ 优点:语法简洁,支持解构赋值,非常适合二分场景。

4. 总结

方法 优点 缺点
groupBy 灵活、可自定义分组逻辑 易出错、需手动处理索引或键
withIndex 更通用、可基于索引分组 代码稍复杂
foldIndexed 性能更优 代码冗长
chunked 简洁、直观、标准库推荐方式 功能有限
windowed 支持滑动窗口、可控制是否包含部分 灵活但略复杂,参数易混淆
partition 适合二分场景,支持解构赋值 仅适用于条件划分

最佳实践建议

  • 优先使用 chunked,它是最简洁且标准推荐的方式。
  • 如果需要更复杂的窗口控制,使用 windowed
  • 如果需要基于条件将列表分为两部分,使用 partition()
  • groupByfoldIndexed 更适合需要高度定制的场景,但要注意代码可读性和性能平衡。

完整的示例代码可以在 GitHub 上找到。


原始标题:Split a List into Parts in Kotlin