Collections | Kotlin - Wyatt's Notes
Collection Hierarchy
Section titled “Collection Hierarchy”Kotlin’s collection types are split into two hierarchies: read-only and mutable.
Collection (read-only) |-- List |-- Set |-- MapMutableCollection extends Collection |-- MutableList |-- MutableSet |-- MutableMapRead-only interfaces do not guarantee immutability — they expose no mutation methods. The Underlying collection may still be mutable through a different reference.
// Read-onlyval readOnly: List<Int> = listOf(1, 2, 3)
// Mutableval mutable: MutableList<Int> = mutableListOf(1, 2, 3)mutable.add(4)mutable[0] = 10List is an ordered collection with index-based access. listOf() returns an immutable list Implementation. mutableListOf() returns a MutableList.
List Operations
Section titled “List Operations”val list = listOf(3, 1, 4, 1, 5, 9)
list[0] // 3list.indexOf(1) // 1 (first occurrence)list.lastIndexOf(1) // 3list.subList(1, 4) // [1, 4, 1]list.reversed() // [9, 5, 1, 4, 1, 3]list.sorted() // [1, 1, 3, 4, 5, 9]list.distinct() // [3, 1, 4, 5, 9]list.contains(4) // trueval readOnly: Set<Int> = setOf(1, 2, 3, 2, 1) // {1, 2, 3}val mutable: MutableSet<Int> = mutableSetOf(1, 2, 3)
val linked: LinkedHashSet<Int> = linkedSetOf(3, 1, 2) // preserves insertion orderval hashed: HashSet<Int> = hashSetOf(3, 1, 2) // no order guaranteeval sorted: TreeSet<Int> = sortedSetOf(3, 1, 2) // natural orderingSet Operations
Section titled “Set Operations”val a = setOf(1, 2, 3, 4)val b = setOf(3, 4, 5, 6)
a union b // {1, 2, 3, 4, 5, 6}a intersect b // {3, 4}a subtract b // {1, 2}a - b // {1, 2}val readOnly: Map<String, Int> = mapOf("a" to 1, "b" to 2, "c" to 3)val mutable: MutableMap<String, Int> = mutableMapOf("a" to 1, "b" to 2)Map Operations
Section titled “Map Operations”val map = mapOf("x" to 1, "y" to 2, "z" to 3)
map["x"] // 1map.getOrDefault("w", 0) // 0map.getOrElse("w") { 42 } // 42map.keys // Set<String> = {x, y, z}map.values // Collection<Int> = [1, 2, 3]map.filterKeys { it != "z" } // {x=1, y=2}map.filterValues { it > 1 } // {y=2, z=3}map.mapKeys { it.key.uppercase() } // {X=1, Y=2, Z=3}map.mapValues { it.value * 2 } // {x=2, y=4, z=6}Map Access Patterns
Section titled “Map Access Patterns”val scores = mutableMapOf("Alice" to 95, "Bob" to 82)
scores.computeIfAbsent("Charlie") { 70 } // inserts if absent, returns valuescores.computeIfPresent("Alice") { _, v -> if (v > 90) v + 5 else v } // updates if present
scores.getOrPut("David") { 60 } // returns existing or inserts and returnsTransformation Operations
Section titled “Transformation Operations”Applies a transformation to each element and returns a new collection.
val names = listOf("alice", "bob", "charlie")val uppercased = names.map { it.uppercase() } // [ALICE, BOB, CHARLIE]
val users = listOf(User("Alice", 30), User("Bob", 25))val namesAndAges = users.map { "${it.name} (${it.age})" }mapIndexed provides the index alongside the element:
val indexed = names.mapIndexed { index, name -> "$index: $name" }// [0: alice, 1: bob, 2: charlie]mapNotNull filters out null results:
val parsed = listOf("1", "two", "3", "four").mapNotNull { it.toIntOrNull() }// [1, 3]filter
Section titled “filter”Returns elements matching the predicate.
val even = listOf(1, 2, 3, 4, 5).filter { it % 2 == 0 } // [2, 4]val nonEmpty = listOf("", "a", "", "bc").filterNot { it.isEmpty() } // [a, bc]
listOf(1, 2, 3, 4, 5).filterIndexed { index, _ -> index % 2 == 0 } // [1, 3, 5]flatMap
Section titled “flatMap”Maps each element to a collection, then flattens the result.
val sentences = listOf("hello world", "kotlin language")val words = sentences.flatMap { it.split(" ") } // [hello, world, kotlin, language]fold and reduce
Section titled “fold and reduce”fold takes an initial accumulator value. reduce uses the first element as the initial value.
val sum = listOf(1, 2, 3, 4, 5).fold(0) { acc, n -> acc + n } // 15val product = listOf(1, 2, 3, 4, 5).reduce { acc, n -> acc * n } // 120
val result = listOf("a", "b", "c").fold(StringBuilder()) { sb, s -> sb.append(s)}.toString() // "abc"reduce throws NoSuchElementException on empty collections. Use reduceOrNull for safe handling.
val empty: Int? = emptyList<Int>().reduceOrNull { a, b -> a + b } // nullgroupBy
Section titled “groupBy”Groups elements by a key and returns a Map<K, List<V>>.
data class Person(val name: String, val city: String, val age: Int)
val people = listOf( Person("Alice", "NYC", 30), Person("Bob", "NYC", 25), Person("Charlie", "LA", 35))
val byCity = people.groupBy { it.city }// {NYC=[Person(Alice, NYC, 30), Person(Bob, NYC, 25)], LA=[Person(Charlie, LA, 35)]}
val avgAgeByCity = people.groupBy( keySelector = { it.city }, valueTransform = { it.age }).mapValues { (_, ages) -> ages.average() }// {NYC=27.5, LA=35.0}partition
Section titled “partition”Splits a collection into two lists based on a predicate.
val (pass, fail) = listOf(85, 42, 91, 67, 55, 98).partition { it >= 60 }// pass = [85, 91, 67, 98], fail = [42, 55]Pairs elements from two collections.
val keys = listOf("a", "b", "c")val values = listOf(1, 2, 3)val pairs = keys zip values // [(a, 1), (b, 2), (c, 3)]val map = keys.zip(values).toMap() // {a=1, b=2, c=3}chunked and windowed
Section titled “chunked and windowed”val data = listOf(1, 2, 3, 4, 5, 6, 7, 8)data.chunked(3) // [[1, 2, 3], [4, 5, 6], [7, 8]]data.windowed(3) // [[1, 2, 3], [2, 3, 4], [3, 4, 5], [4, 5, 6], [5, 6, 7], [6, 7, 8]]data.windowed(3, 3) // [[1, 2, 3], [4, 5, 6], [7, 8]] -- step size 3associate
Section titled “associate”val users = listOf(User("Alice", 30), User("Bob", 25))val byName = users.associate { it.name to it }val byNameV2 = users.associateBy { it.name }val byNameV3 = users.associateBy(keySelector = { it.name }, valueTransform = { it.age })// byNameV3 = {Alice=30, Bob=25}Sequence
Section titled “Sequence”Sequences are lazy — transformations are not executed until a terminal operation is invoked. For Large collections or chains of operations, sequences avoid creating intermediate collections.
val result = (1..1_000_000) .asSequence() .filter { it % 2 == 0 } .map { it * it } .take(5) .toList()// [4, 16, 36, 64, 100]With a list, each intermediate operation creates a new list:
list.filter { ... }.map { ... }.take(5)// Creates: filtered list -> mapped list -> then takes 5With a sequence, each element flows through the entire pipeline before the next element is Processed:
list.asSequence().filter { ... }.map { ... }.take(5)// Processes: element 1 (filter -> map), element 2, ... until 5 collectedWhen to Use Sequences
Section titled “When to Use Sequences”- ** Use sequences when the collection is large and you have multiple chained operations.
- ** Use sequences when you need only a subset of the result (e.g.,
first``take). - ** Use lists when the collection is small or you need to transform the entire collection.
Sequence Creation
Section titled “Sequence Creation”val seq1 = listOf(1, 2, 3).asSequence()val seq2 = sequenceOf(1, 2, 3)val seq3 = generateSequence(1) { it * 2 } // 1, 2, 4, 8, 16, ... (lazy, infinite)val seq4 = generateSequence(seed = 0) { if (it < 100) it + 1 else null } // 0..99Common Pitfalls
Section titled “Common Pitfalls”- ** Chaining multiple operations on large lists without using sequences. This creates intermediate collections at each step, increasing memory pressure and GC overhead.
- ** Using
associateBywhen keys are not unique. Only the last value for each key is retained. UsegroupBywhen you need to keep all values. - ** Modifying a mutable collection while iterating over it. This throws
ConcurrentModificationException. UsetoList()to create a snapshot orremoveIffor conditional removal. - ** Assuming read-only collections are immutable.
List<Int>is a read-only interface; the underlying implementation may be mutable. UsetoList()ortoImmutableList()(Kotlinx Collections) for defensive copies.
flowchart TD
A[Collections] --> B[Key Concepts]
A --> C[Core Principles]
A --> D[Practical Applications]
B --> E[Fundamental definitions]
C --> F[Design patterns]
D --> G[Real-world usage]Summary
Section titled “Summary”This topic covers the core concepts of collections, including underlying theory, practical implementation, and key applications.
Key concepts include:
- core concepts and terminology
- algorithms and computational thinking
- practical implementation
- security and ethical considerations
- applications in the real world
Understanding these concepts thoroughly is essential for both examinations and practical programming, and requires both theoretical knowledge and hands-on practice.
Worked Examples
Section titled “Worked Examples”Worked examples demonstrating the application of key concepts are covered in the detailed sub-pages linked above.
Intuition
Section titled “Intuition”Kotlin collections split into read-only and mutable interfaces, where read-only means no mutation methods are exposed — but the underlying object may still be mutable through another reference. Transformation operations like map, filter, and flatMap create new collections, enabling a functional style without modifying originals. Sequences are lazy versions of collections that process elements one at a time through the entire pipeline, avoiding intermediate collection allocation for large datasets. Grouping, partitioning, and association operations reorganize collections by keys or predicates. The toList() function creates a defensive snapshot, which is essential when modifying a collection during iteration.
Cross-References
Section titled “Cross-References”- Types and Variables — nullable collections vs collections of nullables
- Functions — higher-order functions like map and filter
- Coroutines — Flow as a lazy asynchronous collection