Collections Workbook
Eleven short exercises on Kotlin collections — read-only versus mutable, the factory functions, map access, getOrDefault, the plus and minus operators, and read-only signatures.
Practice problems for Kotlin Collections Are Read-Only Until You Say Otherwise. Each takes a minute or two. Most exercises auto-check: implement the function in the editor and press Run — hidden tests go green when you’re right and red (with a hint) when you’re not, all on JetBrains’ Kotlin compiler server. A few exercises about mutation and observation stay attempt-then-reveal: press Run to try them, then click Show answer. Nothing here is a trick question, just direct practice of the syntax from the lesson.
creating collections
1. A read-only list
Implement numbers so it returns a read-only list holding 1, 2, 3.
import org.junit.Test
import org.junit.Assert
class Test {
@Test fun readOnly() {
Assert.assertEquals("return a read-only list of 1, 2, 3",
listOf(1, 2, 3), numbers())
}
}
//sampleStart
fun numbers(): List<Int> =
emptyList() // TODO: return a read-only list holding 1, 2, 3
//sampleEnd Show answer Hide answer
fun numbers() = listOf(1, 2, 3) 2. A mutable list
Create an empty mutable list of String and add "first" and "second".
fun main() {
val seen = mutableListOf<String>()
// TODO: add "first" and "second" to seen
println(seen)
} Show answer Hide answer
val seen = mutableListOf<String>()
seen.add("first")
seen.add("second") 3. A set drops duplicates
What does setOf("a", "b", "a") contain?
fun main() {
// TODO: run this and see what a Set keeps
val letters = setOf("a", "b", "a")
println(letters)
} Show answer Hide answer
setOf("a", "b", "a") // {"a", "b"}A Set holds each distinct value once.
4. A map
Implement ages so it returns a read-only map from "Ada" to 36 and "Linus" to 54.
import org.junit.Test
import org.junit.Assert
class Test {
@Test fun mapsNames() {
Assert.assertEquals("map Ada to 36 and Linus to 54",
mapOf("Ada" to 36, "Linus" to 54), ages())
}
}
//sampleStart
fun ages(): Map<String, Int> =
emptyMap() // TODO: map "Ada" to 36 and "Linus" to 54
//sampleEnd Show answer Hide answer
fun ages() = mapOf("Ada" to 36, "Linus" to 54) reading and combining
5. A lookup that might miss
Given the map ages, look up "Ada". What’s the type of the result, and why?
fun main() {
val ages = mapOf("Ada" to 36, "Linus" to 54)
// TODO: look up "Ada" — what type comes back?
val age = 0
println(age)
} Show answer Hide answer
val age: Int? = ages["Ada"]It’s Int? — the key might be absent, so the type is nullable.
6. A lookup with a default
Implement lookup so it returns ages[key], falling back to 0 when the key is missing.
import org.junit.Test
import org.junit.Assert
class Test {
@Test fun withDefault() {
val ages = mapOf("Ada" to 36, "Linus" to 54)
Assert.assertEquals("fall back to 0 when the key is missing",
0, lookup(ages, "Nobody"))
Assert.assertEquals("return the value when the key is present",
36, lookup(ages, "Ada"))
}
}
//sampleStart
fun lookup(ages: Map<String, Int>, key: String): Int =
0 // TODO: return ages[key], or 0 when the key is missing
//sampleEnd Show answer Hide answer
fun lookup(ages: Map<String, Int>, key: String) = ages.getOrDefault(key, 0) 7. Add without mutating
Implement appended so it returns a new list with 4 added to the end, leaving the input unchanged.
import org.junit.Test
import org.junit.Assert
class Test {
@Test fun addsFour() {
Assert.assertEquals("append 4 to the end",
listOf(1, 2, 3, 4), appended(listOf(1, 2, 3)))
Assert.assertEquals("append 4 to an empty list",
listOf(4), appended(listOf()))
}
}
//sampleStart
fun appended(nums: List<Int>): List<Int> =
nums // TODO: return a new list with 4 added to the end
//sampleEnd Show answer Hide answer
fun appended(nums: List<Int>) = nums + 4 // [1, 2, 3, 4] 8. Basic reads
Given val numbers = listOf(4, 8, 15), get its size, its first element, and whether it contains 8.
fun main() {
val numbers = listOf(4, 8, 15)
// TODO: print its size, its first element, and whether it contains 8
println(numbers)
} Show answer Hide answer
numbers.size // 3
numbers.first() // 4
8 in numbers // true mutability where it belongs
9. Build mutably, return read-only
Implement labels(n) so it builds ["item 0", "item 1", …] with a mutable list internally but returns a read-only List.
import org.junit.Test
import org.junit.Assert
class Test {
@Test fun buildsLabels() {
Assert.assertEquals("build item 0 through item n-1",
listOf("item 0", "item 1", "item 2"), labels(3))
Assert.assertEquals("an empty list for n = 0",
listOf<String>(), labels(0))
}
}
//sampleStart
fun labels(n: Int): List<String> {
// TODO: build ["item 0", "item 1", ...] with a mutable list, then return it
return emptyList()
}
//sampleEnd Show answer Hide answer
fun labels(n: Int): List<String> {
val result = mutableListOf<String>()
for (i in 0 until n) result.add("item $i")
return result
} 10. A counting map
Implement count(words) so it tallies how many times each word appears, returning a read-only map.
import org.junit.Test
import org.junit.Assert
class Test {
@Test fun tallies() {
Assert.assertEquals("count each word",
mapOf("a" to 2, "b" to 1), count(listOf("a", "b", "a")))
Assert.assertEquals("an empty map for no words",
mapOf<String, Int>(), count(listOf()))
}
}
//sampleStart
fun count(words: List<String>): Map<String, Int> {
// TODO: tally how many times each word appears, then return the map
return emptyMap()
}
//sampleEnd Show answer Hide answer
fun count(words: List<String>): Map<String, Int> {
val tally = mutableMapOf<String, Int>()
for (word in words) {
tally[word] = tally.getOrDefault(word, 0) + 1
}
return tally
} 11. A read-only view still changes
Write code that shows a read-only List reflecting a change made through the underlying MutableList it was assigned from — then take a stable snapshot.
fun main() {
val m = mutableListOf(1, 2)
val view: List<Int> = m
// TODO: mutate m, print view (watch it change), then take a stable snapshot with toList()
println(view)
} Show answer Hide answer
val m = mutableListOf(1, 2)
val view: List<Int> = m
m.add(3)
println(view) // [1, 2, 3] — same object, read-only interface
val snapshot = m.toList() // an independent copy Going deeper: builders, grouping, and read-only reality
12. Group by a key
Implement byInitial so it groups words by their first character — ["ant", "bee", "ape"] becomes {a=[ant, ape], b=[bee]}.
import org.junit.Test
import org.junit.Assert
class Test {
@Test fun byInitial() {
Assert.assertEquals(
mapOf('a' to listOf("ant", "ape"), 'b' to listOf("bee")),
byInitial(listOf("ant", "bee", "ape")))
}
}
//sampleStart
fun byInitial(words: List<String>): Map<Char, List<String>> =
emptyMap() // TODO: group by first character
//sampleEnd Show answer Hide answer
fun byInitial(words: List<String>) = words.groupBy { it.first() }groupBy builds a map from each key to the list of elements sharing it. (associateBy would keep only the last per key instead.)
13. Build imperatively, expose read-only
Use buildList to construct [1, 2, 3, 4] for n = 4 — mutable inside the block, List on the way out.
import org.junit.Test
import org.junit.Assert
class Test {
@Test fun countUp() {
Assert.assertEquals(listOf(1, 2, 3, 4), countUp(4))
Assert.assertEquals(emptyList<Int>(), countUp(0))
}
}
//sampleStart
fun countUp(n: Int): List<Int> =
emptyList() // TODO: build 1..n with buildList
//sampleEnd Show answer Hide answer
fun countUp(n: Int): List<Int> = buildList {
for (i in 1..n) add(i)
}Inside the block you have the full mutable API; the returned type is read-only.
14. Read-only isn’t immutable
You hold a List<Int> that was actually created as a mutableListOf(...). Can Kotlin code force an element onto it? Reveal to check.
Show answer Hide answer
Yes — a read-only List is a compile-time view, not a separate immutable structure. If the underlying object is really a MutableList, a cast reaches it: (list as MutableList<Int>).add(99) compiles and runs. (It only works when the object is genuinely mutable; on a listOf(...), whose runtime type is java.util.Arrays.ArrayList, the add throws.) For a true snapshot nobody can mutate, copy with toList().
Back to the lesson, Kotlin Collections Are Read-Only, or on to the next one: lambdas.
Comments