Advanced Collections (groupBy, fold, windowed)

Reviewed & published by Brayan K

Kotlin's standard library offers powerful collection operators — groupBy, fold, windowed and more — that replace hand-written loops with clear, declarative one-liners.

Part of the free Kotlin course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

You'll group and index data, accumulate with fold/reduce, slice with chunked/windowed, and aggregate with sumOf, maxByOrNull, and zip.

What You'll Learn in This Lesson

1️⃣ Grouping and Indexing

groupBy { key } buckets elements into a Map<K, List<V>> — every member is kept. associateBy { key } builds a Map<K, V> with one value per key, so on a key collision the last element wins.

fun main() {
    val nums = listOf(1, 2, 3, 4, 5)

    // groupBy: bucket items by a key. Keys appear in first-seen order.
    // 1->1, 2->0, 3->1, 4->0, 5->1
    val byParity = nums.groupBy { it % 2 }
    println(byParity)          // -> {1=[1, 3, 5], 0=[2, 4]}

    val words = listOf("apple", "banana", "avocado")

    // associateBy: build a Map keyed by a function.
    // 'a' appears twice; the LAST one (avocado) wins.
    val byFirst = words.associateBy { it.first() }
    println(byFirst)           // -> {a=avocado, b=banana}
}

2️⃣ Accumulating and Splitting

fold(seed) { acc, x -> ... } accumulates from an initial value and can change type; reduce is the seedless version starting from the first element. partition { ... } splits a list into a pair of (matches, non-matches).

fun main() {
    val nums = listOf(1, 2, 3, 4)

    // fold: start from an initial value and accumulate.
    val sum = nums.fold(0) { acc, x -> acc + x }
    println(sum)               // -> 10

    // reduce: like fold but starts from the FIRST element (no seed).
    val product = nums.reduce { acc, x -> acc * x }
    println(product)           // -> 24  (1*2*3*4)

    // partition: split into (matches, non-matches).
    val (big, small) = nums.partition { it > 2 }
    println(big)               // -> [3, 4]
    println(small)             // -> [1, 2]
}

Remember: reduce throws on an empty list (no first element), while fold safely returns the seed.

3️⃣ Slicing, Flattening, Aggregating

chunked(n) makes non-overlapping batches; windowed(n) makes overlapping sliding windows; flatMap expands each element to a list and flattens. Then sumOf, maxByOrNull, and zip aggregate.

fun main() {
    val nums = listOf(1, 2, 3, 4, 5)

    // chunked: break into fixed-size pieces (last may be smaller).
    println(nums.chunked(2))     // -> [[1, 2], [3, 4], [5]]

    // windowed: sliding windows of a given size.
    println(nums.windowed(2))    // -> [[1, 2], [2, 3], [3, 4], [4, 5]]

    // flatMap: map each item to a list, then flatten one level.
    val pairs = listOf(1, 2).flatMap { listOf(it, it * 10) }
    println(pairs)               // -> [1, 10, 2, 20]
}
fun main() {
    val words = listOf("ab", "cde", "f")

    // sumOf: sum a number derived from each item.
    println(words.sumOf { it.length })       // -> 6  (2+3+1)

    // maxByOrNull: the item with the largest derived value.
    println(words.maxByOrNull { it.length }) // -> cde

    // zip: pair up two lists element by element (stops at shorter).
    val letters = listOf("a", "b", "c")
    val ranks = listOf(1, 2, 3)
    println(letters.zip(ranks))              // -> [(a, 1), (b, 2), (c, 3)]
}

Your turn. Fill in the ___ blanks, then run and compare.

// 🎯 YOUR TURN — replace each ___ then run main()
fun main() {
    val nums = listOf(2, 4, 6, 8)

    // 1) Sum them starting from 0 using fold
    val total = nums.___(0) { acc, x -> acc + x }   // 👉 put: fold

    // 2) Split into >5 and the rest using partition
    val (big, small) = nums.___ { it > 5 }          // 👉 put: partition

    println("$total $big $small")

    // ✅ Expected output:
    // 20 [6, 8] [2, 4]
}

Mini-Challenge: A Word Report

Group words by length and total their letters with groupBy and sumOf.

// 🎯 MINI-CHALLENGE: a small word report.
// Save as WordReport.kt and run main().
//
// 1. Group the words by their length.
// 2. Print the groups map.
// 3. Print the total number of letters using sumOf { it.length }.
//
// ✅ Expected output:
//    {3=[cat, dog], 4=[bird]}
//    10

fun main() {
    val words = listOf("cat", "dog", "bird")
    // 1) val byLength = ...
    // 2) println(byLength)
    // 3) println(total letters)
}

Common Errors (and the fix)

Pro Tips

📋 Quick Reference — Operators

OperatorDoes
groupByMap of key to list of members
associateByMap of key to single (last) value
fold / reduceAccumulate to one value
chunked / windowedBatches / sliding windows
flatMapExpand then flatten one level
sumOf / zipSum a selector / pair two lists

🎉 Lesson Complete!

Practice quiz

How do groupBy and associateBy differ on duplicate keys?

  • They behave identically
  • associateBy keeps all; groupBy keeps the first
  • groupBy keeps all members in a list; associateBy keeps only the last
  • Both throw on duplicates

Answer: groupBy keeps all members in a list; associateBy keeps only the last. groupBy gives Map<K, List<V>>; associateBy gives Map<K, V> where the last wins.

What does chunked(2) on [1,2,3,4,5] produce?

  • [[1,2],[3,4],[5]] non-overlapping pieces
  • [[1,2],[2,3],[3,4],[4,5]]
  • [1,2,3,4,5]
  • [[1,2,3],[4,5]]

Answer: [[1,2],[3,4],[5]] non-overlapping pieces. chunked makes consecutive non-overlapping pieces; the last may be smaller.

What does windowed(2) on [1,2,3,4,5] produce?

  • [[1,2],[3,4],[5]]
  • [1,2,3,4,5]
  • [[1,2,3,4,5]]
  • [[1,2],[2,3],[3,4],[4,5]] overlapping windows

Answer: [[1,2],[2,3],[3,4],[4,5]] overlapping windows. windowed makes overlapping sliding windows that move one step at a time.

What does flatMap do compared to map?

  • It only filters
  • It maps each element to a list and flattens one level
  • It sorts the result
  • It is identical to map

Answer: It maps each element to a list and flattens one level. flatMap expands each element to a list then flattens into one flat list.

What does partition { it > 2 } return?

  • A Pair of (matches, non-matches)
  • A single filtered list
  • A Map of groups
  • A boolean

Answer: A Pair of (matches, non-matches). partition splits into a pair: elements that match, and those that don't.

What does reduce do on an empty list, and fold?

  • Both return null
  • Both throw
  • reduce throws; fold returns its seed
  • reduce returns the seed; fold throws

Answer: reduce throws; fold returns its seed. fold is empty-safe via its seed; reduce throws with no first element.

What does maxByOrNull { it.length } return?

  • The largest length value
  • The element with the largest length, or null if empty
  • A sorted list
  • The first element

Answer: The element with the largest length, or null if empty. maxByOrNull returns the element, not the derived value, and null on empty.

What does sumOf { it.length } do for a list of strings?

  • Counts the strings
  • Returns the longest string
  • Concatenates the strings
  • Applies the selector to each and sums those numbers

Answer: Applies the selector to each and sums those numbers. sumOf sums a number derived from each element without an intermediate map.

What does zip do with two lists?

  • Concatenates them
  • Pairs them element by element, stopping at the shorter
  • Sorts both
  • Removes duplicates

Answer: Pairs them element by element, stopping at the shorter. zip pairs elements positionally and stops at the shorter list.

In groupBy, in what order do the keys appear?

  • Sorted ascending
  • Random order
  • First-seen (insertion) order
  • Reverse order

Answer: First-seen (insertion) order. Keys appear in the order they were first encountered.

Continue this course

Frequently asked questions

What is the difference between groupBy and associateBy?

Both build a Map from a key function, but they handle collisions differently. groupBy { keyFn } produces a Map<K, List<V>> where every element that shares a key is collected into a list — nothing is lost. associateBy { keyFn } produces a Map<K, V> with a single value per key, so when two elements map to the same key the LAST one wins and earlier ones are overwritten. Use groupBy when you want all members of each bucket; use associateBy when each key should identify exactly one element, like indexing users by id.

When should I use fold instead of reduce?

Both combine a collection into a single value, but reduce starts from the first element and requires the result type to match the element type, while fold takes an explicit initial (seed) value and can therefore produce a different type. Two practical consequences: fold works on an empty collection (it returns the seed) whereas reduce throws on an empty collection because there is no first element, and fold lets you accumulate into something else entirely, like folding a list of strings into a single combined length Int. Reach for fold by default; reduce is a concise special case.

What is the difference between chunked and windowed?

chunked(n) splits a collection into consecutive, NON-overlapping pieces of size n, with the final piece possibly smaller — chunked(2) on [1,2,3,4,5] gives [[1,2],[3,4],[5]]. windowed(n) produces OVERLAPPING sliding windows of exactly size n that move one step at a time — windowed(2) on the same list gives [[1,2],[2,3],[3,4],[4,5]]. Use chunked to batch items (for example processing 100 records at a time) and windowed for neighbour comparisons like moving averages or detecting rising sequences.

What does flatMap do compared to map?

map { } applies a function to each element and returns a list of the same length — if the function returns a list, you end up with a list of lists. flatMap { } applies a function that returns a list for each element and then flattens the results by one level into a single list. So mapping [1,2] to listOf(it, it*10) gives [[1,10],[2,20]], while flatMap gives [1,10,2,20]. Use flatMap whenever each input expands into several outputs you want combined into one flat collection — for instance gathering all the tags across many posts.

How do sumOf and maxByOrNull differ from sum and max?

sum() and the older max() work directly on collections of numbers. sumOf { selector } first applies a selector to each element and sums those numbers, so you can sum a derived value like the length of each string without an intermediate map. maxByOrNull { selector } returns the ELEMENT whose selector value is largest (not the value itself), and returns null on an empty collection rather than throwing — which is why it replaced the older max-throwing variants. There is a matching minByOrNull, plus maxOf/minOf when you want the largest derived value rather than the element.