LearnAI ToolsCareerPractice BuildsPlayContact
Lesson 917 min read

Collections

Learn Kotlin's List, Set, and Map types, mutable vs read-only collections, and the standard library's functional operators like map, filter, and reduce.

Introduction

Kotlin's collection types build directly on top of Java's existing collection framework — a `List<String>` in Kotlin is a real `java.util.List` underneath. What Kotlin adds is a crucial distinction Java lacks: separate read-only and mutable interfaces for every collection type, plus a rich set of functional operators for transforming data without writing manual loops.

This lesson covers List, Set, and Map, the read-only/mutable distinction, and the functional-style operators — `map`, `filter`, `forEach`, `reduce`, and sorting — that make working with collections in Kotlin dramatically more concise than in Java.

Read-Only vs Mutable Collections

Every collection type in Kotlin has two interfaces: a read-only one (`List`, `Set`, `Map`) that exposes no way to add, remove, or change elements, and a mutable one (`MutableList`, `MutableSet`, `MutableMap`) that does. This is a design-level safety feature, similar in spirit to `val` vs `var` — it lets you expose a collection to other code with a guarantee it cannot be modified.

fun main() {
val readOnly: List<String> = listOf("Kotlin", "Java")
val mutable: MutableList<String> = mutableListOf("Kotlin", "Java")
// readOnly.add("Swift") // Compile error: List has no add() method
mutable.add("Swift") // OK
println(readOnly)
println(mutable)
}
Output

Click Run to see what this code prints.

Not the Same as Immutable

A read-only `List` is not necessarily immutable — it might be backed by a `MutableList` elsewhere that another part of the code changes. `List` only guarantees that this particular reference has no mutating methods; it does not guarantee the underlying data never changes.

List

A `List` is an ordered collection that allows duplicate elements, accessed by index. `listOf()` creates a read-only list; `mutableListOf()` creates a mutable one.

fun main() {
val fruits = listOf("Apple", "Banana", "Apple", "Cherry")
println(fruits[0]) // indexed access
println(fruits.size)
println(fruits.contains("Banana"))
}
Output

Click Run to see what this code prints.

Set

A `Set` is an unordered collection that automatically discards duplicate elements. `setOf()` creates a read-only set; `mutableSetOf()` creates a mutable one.

fun main() {
val uniqueNumbers = setOf(1, 2, 2, 3, 3, 3)
println(uniqueNumbers) // duplicates removed automatically
println(uniqueNumbers.size)
}
Output

Click Run to see what this code prints.

Map

A `Map` stores key-value pairs with unique keys. `mapOf()` creates a read-only map; `mutableMapOf()` creates a mutable one. Kotlin's `to` infix function builds a `Pair`, which is the idiomatic way to write map entries.

fun main() {
val ages = mapOf("Alice" to 30, "Bob" to 25)
println(ages["Alice"]) // 30
println(ages["Unknown"]) // null — no exception
println(ages.getOrDefault("Unknown", 0)) // 0
for ((name, age) in ages) {
println("$name is $age")
}
}
Output

Click Run to see what this code prints.

Functional Operators: map, filter, forEach

Kotlin's standard library adds a large set of functional-style extension functions to every collection, letting you transform data declaratively instead of writing manual loops. `map` transforms each element, `filter` keeps only elements matching a condition, and `forEach` runs an action on each element.

fun main() {
val numbers = listOf(1, 2, 3, 4, 5, 6)
val doubled = numbers.map { it * 2 }
val evens = numbers.filter { it % 2 == 0 }
println(doubled)
println(evens)
evens.forEach { println("Even: $it") }
}
Output

Click Run to see what this code prints.

These operators can be chained fluently, since each one returns a new collection rather than mutating the original — `numbers.filter { it % 2 == 0 }.map { it * it }` reads almost like a sentence: "filter the numbers to evens, then square each one."

reduce and fold

`reduce` combines all elements of a collection into a single value by repeatedly applying an operation, using the first element as the starting accumulator. `fold` works the same way but lets you supply your own starting value, which also makes it safe to use on empty collections.

fun main() {
val numbers = listOf(1, 2, 3, 4)
val sum = numbers.reduce { acc, n -> acc + n }
val sumWithBase = numbers.fold(100) { acc, n -> acc + n }
println(sum)
println(sumWithBase)
}
Output

Click Run to see what this code prints.

Sorting Collections

`sorted()` sorts elements using their natural order; `sortedBy` and `sortedByDescending` sort using a selector you provide, which is especially useful for sorting objects by a specific property.

data class Employee(val name: String, val salary: Int)
fun main() {
val employees = listOf(
Employee("Nina", 75000),
Employee("Omar", 62000),
Employee("Priya", 91000)
)
val bySalary = employees.sortedByDescending { it.salary }
bySalary.forEach { println("${it.name}: ${it.salary}") }
}
Output

Click Run to see what this code prints.

Common Mistakes

Avoid These Mistakes
  • Assuming `List` means immutable — it only means "no mutating methods on this reference," not that the underlying data can never change.
  • Using `mutableListOf()` by default out of habit — prefer read-only `listOf()` unless you specifically need to add/remove elements.
  • Chaining many `map`/`filter` calls on very large collections without considering `asSequence()`, which processes elements lazily instead of building an intermediate list at every step.
  • Forgetting that map access with `[]` returns null for a missing key instead of throwing, unlike some other languages' map/dictionary types.

Best Practices

  • Default to read-only collection types (`List`, `Set`, `Map`) for anything you expose outside a function or class.
  • Prefer `map`/`filter`/`reduce`-style operators over manual `for` loops with an accumulator — they are more concise and less error-prone.
  • Use `getOrDefault` or the Elvis operator (`map[key] ?: default`) instead of assuming a map lookup always succeeds.
  • Reach for `sortedBy { it.property }` instead of writing a manual `Comparator` for simple property-based sorting.

Frequently Asked Questions

Under the hood, yes — Kotlin's List, Set, and Map are backed by the same java.util collection classes. Kotlin adds the read-only/mutable interface split and a large library of extension functions on top.

`map` transforms each element and returns a new collection of the results; `forEach` just performs an action on each element (like printing) and returns nothing useful (Unit).

Use a Set when you need automatic duplicate removal and don't care about element order or index-based access; use a List when order and duplicates matter, or when you need to access elements by position.

Key Takeaways

  • Kotlin splits every collection into a read-only interface (List, Set, Map) and a mutable one (MutableList, MutableSet, MutableMap).
  • List preserves order and allows duplicates; Set automatically removes duplicates; Map stores unique keys mapped to values.
  • Functional operators like `map`, `filter`, and `forEach` replace most manual loops with concise, chainable expressions.
  • `reduce` and `fold` combine a collection into a single value; `fold` additionally accepts a starting value.
  • `sortedBy`/`sortedByDescending` sort collections using a selector, without writing a manual Comparator.

Summary

You can now store, transform, and query data confidently using Kotlin's List, Set, and Map types and its functional-style operators. Next, you'll learn two of Kotlin's most distinctive class types: data classes and sealed classes.

Next Lesson →

Data Classes & Sealed Classes