Skip to content
beginner Phase 1 · Kotlin Foundations

Collections

Work with List, Set, Map, and their mutable variants. Use map, filter, reduce, and other collection operations.

50m
4 problems
Topic Progress 0%

List, Set, and Map

Read-Only vs Mutable Collections

Kotlin separates read-only and mutable collections at the type level. A List<T> provides only read operations (get, size, contains). A MutableList<T> adds write operations (add, remove, set). This is a design decision, not a technical limitation: the underlying JVM collection is the same, but the Kotlin compiler restricts what you can do through the read-only interface.

val readOnlyList = listOf(1, 2, 3)
// readOnlyList.add(4)  // Compile error

val mutableList = mutableListOf(1, 2, 3)
mutableList.add(4)      // Allowed

This distinction matters because it documents intent. If a function receives List<T>, the caller knows the function will not modify it. If it receives MutableList<T>, modification is expected.

List

Lists are ordered collections that allow duplicates. Use listOf for immutable lists and mutableListOf for mutable ones:

val fruits = listOf("apple", "banana", "cherry")
println(fruits[0])        // apple
println(fruits.size)       // 3
println(fruits.contains("banana"))  // true

// Indexed access with null safety
val second: String? = fruits.getOrNull(1)  // banana
val tenth: String? = fruits.getOrNull(9)   // null

Use listOfNotNull to create a list that filters out nulls:

val mixed = listOfNotNull(1, null, 2, null, 3)
println(mixed)  // [1, 2, 3]

Set

Sets are unordered collections that reject duplicates. Use setOf and mutableSetOf:

val uniqueNumbers = setOf(1, 2, 2, 3, 3, 3)
println(uniqueNumbers)  // [1, 2, 3]
println(uniqueNumbers.size)  // 3

val mutableSet = mutableSetOf("a", "b")
mutableSet.add("c")
mutableSet.add("a")  // Duplicate ignored
println(mutableSet)  // [a, b, c]

Map

Maps store key-value pairs. Keys must be unique; values can repeat:

val capitals = mapOf(
    "USA" to "Washington",
    "Japan" to "Tokyo",
    "Germany" to "Berlin"
)

println(capitals["USA"])          // Washington
println(capitals.getOrDefault("France", "Unknown"))  // Unknown

// Indexed access returns null for missing keys
val capital: String? = capitals["Brazil"]  // null

Use to to create pairs. The mutableMapOf variant allows modification:

val mutableCapitals = mutableMapOf(
    "USA" to "Washington"
)
mutableCapitals["Japan"] = "Tokyo"
mutableCapitals.remove("USA")

Construction Functions

Kotlin provides several ways to create collections:

val empty = emptyList<String>()
val singleton = listOf("only")
val filled = List(5) { it * 2 }  // [0, 2, 4, 6, 8]
val fromArray = listOf(*arrayOf(1, 2, 3))

The List(n) { transform } constructor creates a list of size n with each element computed by the transform lambda, where it is the index.

Transformation and Aggregation

Map: Transforming Elements

map applies a function to each element and returns a new list with the results:

val numbers = listOf(1, 2, 3, 4, 5)
val doubled = numbers.map { it * 2 }
println(doubled)  // [2, 4, 6, 8, 10]

// With index
val indexed = numbers.mapIndexed { index, value ->
    "$index: $value"
}
println(indexed)  // [0: 1, 1: 2, 2: 3, 3: 4, 4: 5]

Filter: Selecting Elements

filter keeps elements that match a predicate. filterNot keeps elements that do not match:

val numbers = listOf(1, 2, 3, 4, 5, 6)
val evens = numbers.filter { it % 2 == 0 }
println(evens)  // [2, 4, 6]

val odds = numbers.filterNot { it % 2 == 0 }
println(odds)   // [1, 3, 5]

Reduce and Fold: Accumulating Results

reduce combines elements left-to-right using a function. The function takes the accumulated value and the current element:

val numbers = listOf(1, 2, 3, 4, 5)
val sum = numbers.reduce { acc, current -> acc + current }
println(sum)  // 15

fold is like reduce but starts with an initial value, allowing the accumulator type to differ from the element type:

val words = listOf("Hello", "World")
val sentence = words.fold("") { acc, word ->
    if (acc.isEmpty()) word else "$acc $word"
}
println(sentence)  // Hello World

Chaining Operations

Collection operations return new collections, so you can chain them for data pipelines:

val result = listOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
    .filter { it % 2 == 0 }     // [2, 4, 6, 8, 10]
    .map { it * it }             // [4, 16, 36, 64, 100]
    .filter { it > 20 }          // [36, 64, 100]
    .sum()                       // 200

Each operation produces an intermediate list. For performance-critical code, use asSequence() to chain operations lazily.

Grouping and Partitioning

groupBy groups elements by a key. partition splits into two lists based on a predicate:

val words = listOf("apple", "banana", "avocado", "blueberry")
val grouped = words.groupBy { it.first() }
println(grouped)  // {a=[apple, avocado], b=[banana, blueberry]}

val (startsA, rest) = words.partition { it.startsWith('a') }
println(startsA)  // [apple, avocado]
println(rest)     // [banana, blueberry]

Other Useful Operations

val numbers = listOf(3, 1, 4, 1, 5, 9, 2, 6)

println(numbers.sorted())       // [1, 1, 2, 3, 4, 5, 6, 9]
println(numbers.distinct())     // [3, 1, 4, 5, 9, 2, 6]
println(numbers.take(3))        // [3, 1, 4]
println(numbers.drop(3))        // [1, 5, 9, 2, 6]
println(numbers.any { it > 5 }) // true
println(numbers.all { it > 0 }) // true
println(numbers.none { it > 10 }) // true
println(numbers.count { it % 2 == 0 }) // 3
println(numbers.joinToString(", ")) // 3, 1, 4, 1, 5, 9, 2, 6

Practice Problems

0 / 3 solved
List Transformer Pipeline

Given a list of integers, return a list of only the even numbers, each squared, in ascending order.

Solution
fun transformNumbers(numbers: List<Int>): List<Int> {
    return numbers
        .filter { it % 2 == 0 }
        .map { it * it }
        .sorted()
}

fun main() {
    val result = transformNumbers(listOf(5, 3, 8, 1, 4, 7, 2))
    println(result)  // [4, 16, 64]
}
Word Frequency Counter

Write a function that takes a list of words and returns a Map<String, Int> counting how many times each word appears. Use groupBy and size, or associateWith.

Solution
fun countWords(words: List<String>): Map<String, Int> {
    return words.groupBy { it }.mapValues { it.value.size }
}

// Alternative with fold
fun countWordsFold(words: List<String>): Map<String, Int> {
    return words.fold(emptyMap()) { acc, word ->
        acc + (word to (acc[word] ?: 0) + 1)
    }
}

fun main() {
    val words = listOf("apple", "banana", "apple", "cherry", "banana", "apple")
    println(countWords(words))  // {apple=3, banana=2, cherry=1}
}
Nested List Flattener

Write a function that takes a List<List<Int>> and flattens it into a single List<Int> without using the built-in flatten() function.

Solution
fun flattenNested(nested: List<List<Int>>): List<Int> {
    return nested.flatMap { it }
}

// Manual implementation
fun flattenManual(nested: List<List<Int>>): List<Int> {
    val result = mutableListOf<Int>()
    for (sublist in nested) {
        result.addAll(sublist)
    }
    return result
}

fun main() {
    val nested = listOf(listOf(1, 2), listOf(3, 4, 5), listOf(6))
    println(flattenNested(nested))  // [1, 2, 3, 4, 5, 6]
}

Quiz

1. What is the difference between List and MutableList in Kotlin?

Question 1 options

2. What does `listOf(1, 2, 3).filter { it > 1 }.map { it * 10 }` return?

Question 2 options

3. What is the difference between reduce and fold?

Question 3 options

4. What does `setOf(1, 1, 2, 2, 3).size` return?

Question 4 options

Flashcards

Question

What are the three main collection types in Kotlin?

Answer

List (ordered, allows duplicates), Set (unordered, no duplicates), Map (key-value pairs, unique keys). Each has a read-only and mutable variant.

Question

What does map() do on a collection?

Answer

Applies a function to each element and returns a new list with the transformed results. Does not modify the original collection.

Question

What is the difference between filter and filterNot?

Answer

filter keeps elements matching the predicate. filterNot keeps elements that do NOT match the predicate. They are logical inverses.

Question

When should you use asSequence() with collection operations?

Answer

When chaining many operations on large collections. asSequence() processes elements lazily, avoiding intermediate list allocation. Use it for performance-critical pipelines.

Revision Notes

Key Takeaways

  • 1. Prefer read-only collections (List, Set, Map) unless mutation is required.
  • 2. Chain filter, map, and reduce for readable data pipelines.
  • 3. fold is more flexible than reduce because it accepts an initial value.
  • 4. Use groupBy to build maps from lists of elements.
  • 5. Use asSequence() for lazy evaluation on large datasets.

Interview Tips

  • Know the time complexity of common operations: get O(1) for list, add O(1) for mutableList, contains O(1) for set.
  • Be ready to explain why Kotlin separates read-only and mutable collection types.
  • Practice writing filter-map-reduce chains for data transformation problems.
  • Discuss when to use Set vs List vs Map for a given problem.

Cheat Sheet

Collections Cheat Sheet

Types:

  • listOf() / mutableListOf() — ordered, duplicates
  • setOf() / mutableSetOf() — unordered, unique
  • mapOf() / mutableMapOf() — key-value pairs

Key Operations:

  • map { } — transform elements
  • filter { } / filterNot { } — select/exclude
  • reduce { acc, i -> } — accumulate from first
  • fold(initial) { acc, i -> } — accumulate from initial
  • flatMap { } — flatten nested collections
  • groupBy { } — group by key
  • partition { } — split by predicate
  • sorted(), distinct(), take(n), drop(n)

Null Handling:

  • listOfNotNull() filters out nulls
  • getOrNull(index) returns null instead of exception