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
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]
} 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}
} 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?
2. What does `listOf(1, 2, 3).filter { it > 1 }.map { it * 10 }` return?
3. What is the difference between reduce and fold?
4. What does `setOf(1, 1, 2, 2, 3).size` return?
Flashcards
Question
What are the three main collection types in Kotlin?
Click to reveal answer
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?
Click to reveal answer
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?
Click to reveal answer
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?
Click to reveal answer
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, duplicatessetOf()/mutableSetOf()— unordered, uniquemapOf()/mutableMapOf()— key-value pairs
Key Operations:
map { }— transform elementsfilter { }/filterNot { }— select/excludereduce { acc, i -> }— accumulate from firstfold(initial) { acc, i -> }— accumulate from initialflatMap { }— flatten nested collectionsgroupBy { }— group by keypartition { }— split by predicatesorted(),distinct(),take(n),drop(n)
Null Handling:
listOfNotNull()filters out nullsgetOrNull(index)returns null instead of exception