LearnAI ToolsCareerPractice BuildsPlayDevelopmentContact
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.

On this page 13 sections

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