Kotlin Collection Operations

Transform, filter, sort, group and summarise collections in one readable line with map, filter, sumOf, sortedBy, groupBy, associate and friends.

Lesson 11 of 43Intermediate18 min read

Much of everyday programming is reshaping collections. Keep the products that are in stock. Turn a list of users into a list of email addresses. Add up the prices in a basket. Sort players by score. Group orders by customer. You could write a for loop for each job, but Kotlin's standard library has a function for almost every one of them.

These collection operations each do one clear job and return a new collection, so they can be chained into a readable pipeline: orders.filter { it.paid }.sumOf { it.total } reads almost like English. They take a small block of code called a lambda, written in curly braces, which you'll explore fully in the lambdas lesson. For now, know that inside the braces it means "the current item".

In this lesson you'll use the most important operations on a realistic list of products, and see how they replace loops with shorter, clearer code.

Transforming and filtering

FunctionDoesExample result
map { }Transforms every itemlistOf(1, 2).map { it * 10 } gives [10, 20]
filter { }Keeps items where the block is truelistOf(1, 2, 3).filter { it > 1 } gives [2, 3]
filterNot { }Keeps items where it's false
mapNotNull { }Transforms and drops nullslistOf("1", "x").mapNotNull { it.toIntOrNull() } gives [1]
forEach { }Runs code for each item, returns nothing

The original collection is never changed; each operation returns a new one.

Questions and summaries

  • any { }, all { } and none { } answer yes/no questions about the items.
  • find { } (or firstOrNull { }) returns the first matching item, or null.
  • count { } counts matching items.
  • sumOf { } adds up a value from each item, such as basket.sumOf { it.price }.
  • maxBy { } and minBy { } return the item with the largest or smallest value; maxOf { } returns the value itself.
  • average() works on lists of numbers.

Sorting and grouping

  • sortedBy { it.price } and sortedByDescending { it.price } sort by any value; chain thenBy comparisons with sortedWith(compareBy({ it.a }, { it.b })).
  • groupBy { it.category } builds a Map<Category, List<Item>>.
  • groupingBy { it }.eachCount() counts how many of each value there are.
  • associateBy { it.id } builds a lookup map from a key to each item; associate { it.id to it.name } builds any key-value map.
  • partition { } splits a list into a Pair of matching and non-matching lists.

Chunking, zipping and flattening

  • chunked(3) splits a list into lists of at most three items, which is useful for pages and rows.
  • zip(other) pairs items from two lists by position: names with scores.
  • flatMap { } maps each item to a list and joins all the lists together, such as every tag from every post.
  • distinct() removes duplicates while keeping a list.
  • take, drop, takeWhile and dropWhile trim from the start.

fold, reduce and joinToString

sum and count cover the common totals, but sometimes you need to combine items your own way. fold starts from a value you choose and feeds each item into a running result:

Kotlin
val nums = listOf(3, 5, 2)
nums.fold(0) { total, n -> total + n }        // 10
nums.reduce { product, n -> product * n }     // 30
nums.runningFold(0) { total, n -> total + n } // [0, 3, 8, 10]

The lambda gets two values: the result so far and the next item. reduce is the same idea, but it uses the first item as the starting value. That makes reduce throw an UnsupportedOperationException ("Empty collection can't be reduced.") on an empty list, whereas fold simply returns its starting value. If the list might be empty, use fold, or reduceOrNull, which returns null. runningFold keeps every step, which is handy for running balances.

To turn a collection into text, joinToString beats a loop that glues strings together:

Kotlin
nums.joinToString()                                   // 3, 5, 2
nums.joinToString(" + ", prefix = "(", postfix = ")") // (3 + 5 + 2)
nums.joinToString(" | ") { "£$it" }                   // £3 | £5 | £2

The optional lambda transforms each item before it's joined, so you don't need a separate map.

When you need an item's position as well as its value, most operations have an Indexed version: mapIndexed { i, s -> "${i + 1}. $s" } numbers a list, and filterIndexed can keep every other item. windowed(3) gives overlapping groups such as [1, 2, 3], [2, 3, 4], which suits moving averages, whereas chunked(3) never overlaps.

Kotlin Collections Syntax

Collection operations

list.map { it * 2 }
list.filter { it > 0 }
list.mapNotNull { it.toIntOrNull() }
list.any { it > 9 }   list.all { it > 0 }   list.count { it > 5 }
list.find { it.name == "Ana" }
list.sumOf { it.price }   list.maxBy { it.score }
list.sortedBy { it.price }   list.sortedByDescending { it.price }
list.groupBy { it.category }   list.groupingBy { it }.eachCount()
list.associateBy { it.id }   list.partition { it.paid }
list.chunked(3)   a.zip(b)   list.flatMap { it.tags }   list.distinct()

Kotlin Collections Summary

  • map transforms, filter keeps, and mapNotNull transforms while dropping nulls.
  • any, all, none, find and count answer questions; sumOf, maxBy and average summarise.
  • sortedBy and sortedByDescending sort by any property.
  • groupBy, groupingBy().eachCount(), associateBy and partition reorganise data.
  • Operations return new collections and chain into readable pipelines.

Kotlin Collections Examples

Each example shows the code, the output it produces and what each part does.

Example 1

Filtering, mapping and summing

Kotlin
data class Product(val name: String, val category: String, val price: Double, val stock: Int)

fun main() {
    val products = listOf(
        Product("Notebook", "Stationery", 3.50, 120),
        Product("Pen", "Stationery", 1.20, 300),
        Product("Backpack", "Bags", 34.99, 15),
        Product("Lunch box", "Kitchen", 8.75, 0),
        Product("Water bottle", "Kitchen", 12.00, 40)
    )

    val inStock = products.filter { it.stock > 0 }
    println(inStock.map { it.name })
    println("Cheapest: ${products.minBy { it.price }.name}")
    println("Any over £30? ${products.any { it.price > 30 }}")
    println("Stock value: £${"%.2f".format(products.sumOf { it.price * it.stock })}")
}
Output
[Notebook, Pen, Backpack, Water bottle]
Cheapest: Pen
Any over £30? true
Stock value: £1784.85
Line-by-line explanation
  1. Line 1A data class holds each product's details; you'll learn them in a later lesson.
  2. Line 12filter keeps the four products with stock left.
  3. Line 13map turns each product into just its name.
  4. Line 16sumOf adds price times stock for every product: 420 + 360 + 524.85 + 0 + 480.
Example 2

Sorting and grouping

Kotlin
data class Product(val name: String, val category: String, val price: Double)

fun main() {
    val products = listOf(
        Product("Notebook", "Stationery", 3.50),
        Product("Pen", "Stationery", 1.20),
        Product("Backpack", "Bags", 34.99),
        Product("Water bottle", "Kitchen", 12.00)
    )

    println(products.sortedByDescending { it.price }.map { it.name })

    val byCategory = products.groupBy { it.category }
    for ((category, items) in byCategory) {
        println("$category: ${items.joinToString { it.name }}")
    }

    val (cheap, pricey) = products.partition { it.price < 10 }
    println("${cheap.size} cheap, ${pricey.size} pricey")
}
Output
[Backpack, Water bottle, Notebook, Pen]
Stationery: Notebook, Pen
Bags: Backpack
Kitchen: Water bottle
2 cheap, 2 pricey
Line-by-line explanation
  1. Line 11Sort by price, highest first, then keep just the names.
  2. Line 13groupBy builds a map from each category to its list of products.
  3. Line 15joinToString with a block chooses what to show for each item; the default separator is ", ".
  4. Line 18partition splits the list into two, unpacked into separate variables.
Example 3

Counting, lookups and zipping

Kotlin
fun main() {
    val votes = listOf("red", "blue", "red", "green", "red", "blue")
    println(votes.groupingBy { it }.eachCount())

    val ids = listOf(101, 102, 103)
    val names = listOf("Ana", "Ben", "Cy")
    val byId = ids.zip(names).toMap()
    println(byId[102])

    val posts = listOf(listOf("kotlin", "android"), listOf("kotlin", "server"))
    println(posts.flatMap { it }.distinct())

    println((1..7).toList().chunked(3))
}
Output
{red=3, blue=2, green=1}
Ben
[kotlin, android, server]
[[1, 2, 3], [4, 5, 6], [7]]
Line-by-line explanation
  1. Line 3groupingBy with eachCount counts each colour in one line.
  2. Line 7zip pairs ids with names by position; toMap turns the pairs into a map.
  3. Line 11flatMap joins the lists of tags, then distinct removes duplicates.
  4. Line 13chunked splits the numbers into groups of three; the last group is shorter.

Common Kotlin Collections Mistakes

Using forEach to build a new list

Don't
val names = mutableListOf<String>()
users.forEach { names.add(it.name) }
Do
val names = users.map { it.name }

Expecting filter to change the list

Don't
val nums = mutableListOf(1, 2, 3, 4)
nums.filter { it % 2 == 0 }
println(nums)                       // [1, 2, 3, 4]
Do
val evens = nums.filter { it % 2 == 0 }
println(evens)                      // [2, 4]

first on a list that might have no match

Don't
val admin = users.first { it.isAdmin }        // NoSuchElementException if none
Do
val admin = users.firstOrNull { it.isAdmin } ?: return

Kotlin Collections in Real Projects

Collection operations in practice

Kotlin code in Android apps and servers is full of these pipelines: filtering search results, mapping API responses into screen models, grouping transactions by month, summing order totals. They make intent obvious at a glance, and because they never change the original collection, they work well with the immutable data that modern apps favour.

Kotlin Collections FAQ

What is the difference between map and forEach in Kotlin?

map returns a new list containing the result of the lambda for every item. forEach returns nothing; it's only for side effects such as printing or saving. If you find yourself adding to a list inside forEach, map or filter will do the job in one step.

What is the difference between fold and reduce?

fold takes an explicit starting value, so it works on empty lists and can produce a different type from the items, such as building a String from numbers. reduce starts from the first item, so the result has the item type, and it throws on an empty list. reduceOrNull is the safe version.

How do I find the most common item in a Kotlin list?

Count each value with groupingBy { it }.eachCount(), then pick the entry with the biggest count: list.groupingBy { it }.eachCount().maxByOrNull { it.value }?.key. The OrNull version returns null for an empty list instead of throwing.

Are Kotlin collection operations slow because they create new lists?

For everyday sizes, no: each step is a simple loop, and the code is far easier to read. With very large lists and long chains, the intermediate lists do cost memory. Calling asSequence() first makes the steps run lazily, one item at a time, and toList() at the end collects the result.

How do I filter out null values from a list in Kotlin?

Call filterNotNull(), which turns a List<String?> into a List<String> with the null items removed. If you're transforming at the same time, mapNotNull { } applies the lambda and drops any null results, as in inputs.mapNotNull { it.toIntOrNull() }.

Kotlin Collections Exercises

Answer each one and check it instantly. If you get stuck, open the hint before the answer.

Fill in the blankseasy

Complete the code so it keeps only numbers greater than 10.

val big = numbers. { it > 10 }
Predict the outputeasy

What does this print?

println(listOf(1, 2, 3).map { it * it })
Predict the outputmedium

What does this print?

val words = listOf("apple", "kiwi", "banana", "fig")
println(words.filter { it.length > 3 }.map { it.uppercase() })
Multiple choicemedium

Which returns a Map from each category to its products?

Predict the outputhard

What does this print?

val (even, odd) = (1..6).partition { it % 2 == 0 }
println(even.sum() - odd.sum())

Kotlin Collections Quiz

5 questions to check what you learned.

  1. Do map and filter change the original list?

    • Yes
    • Only mutable lists
    • Only map does
    • No, they return new lists
  2. What does find { } return when nothing matches?

    • null
    • An empty list
    • An exception
    • The first item
  3. What does listOf("a", "b", "a").groupingBy { it }.eachCount() return?

    • [a, b]
    • 3
    • {2=a, 1=b}
    • {a=2, b=1}
  4. What does sumOf { it.price } do?

    • Adds up the price of every item
    • Sorts by price
    • Finds the highest price
    • Counts items with a price
  5. When is asSequence() useful?

    • For tiny lists
    • To make a list mutable
    • For very large collections or long chains, so items are processed one at a time without building intermediate lists
    • To sort faster