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.

Implement it, then press Run to check 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".

Try it — edit, then press Run 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?

Try it — edit, then press Run 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.

Implement it, then press Run to check 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?

Try it — edit, then press Run 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.

Implement it, then press Run to check 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.

Implement it, then press Run to check 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.

Try it — edit, then press Run 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.

Implement it, then press Run to check 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.

Implement it, then press Run to check 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.

Try it — edit, then press Run 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]}.

Implement it, then press Run to check 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.

Implement it, then press Run to check 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