Practice
Kotlin exercises
Make a change, predict what it will do, and compare the result.
- 1
Kotlin Basics
Use values and string templates.
Adapt the lesson example. Change one value or element, predict the result, and explain why it changes.
Show starting example
Kotlinfun main() { val name = "Ada" println("Hello, $name") } - 2
Kotlin Lists
Process an ordered collection.
Adapt the lesson example. Change one value or element, predict the result, and explain why it changes.
Show starting example
Kotlinfun main() { val scores = listOf(82, 91, 76) for (score in scores) { println(score) } } - 3
Kotlin Functions
Declare typed parameters and a return type.
Predict average(listOf(1, 2)).
Reveal answer and reasoning
The sum is 3, and 3.0 / 2 is 1.5.
Show starting example
Kotlinfun average(values: List<Int>): Double { return values.sum().toDouble() / values.size } fun main() { println(average(listOf(82, 91, 76, 95))) } - 4
Kotlin when Expressions
Choose a result from several conditions with when.
Add a D grade for scores of 70 or more.
Reveal answer and reasoning
Insert score >= 70 -> "D" before else. grade(60) still returns C.
Show starting example
Kotlinfun grade(score: Int): String = when { score >= 90 -> "A" score >= 80 -> "B" else -> "C" } fun main() { println(grade(95)) println(grade(82)) println(grade(60)) } - 5
Kotlin null safety
Distinguish nullable from non-null types, and use ?., ?:, let, and !! deliberately.
What does null?.length return?
Reveal answer and reasoning
null. The safe call skips the property access when the receiver is null, and the result type is Int?.
Show starting example
Kotlinfun describe(name: String?): String { val length = name?.length ?: -1 return name?.let { "name '$it' has $length characters" } ?: "no name" } fun main() { println(describe("Ada")) println(describe(null)) val items: List<String?> = listOf("a", null, "c") println(items.filterNotNull().joinToString("+")) val text: String? = "kotlin" println(text!!.uppercase()) val missing: String? = null val result = runCatching { missing!!.length } println(result.exceptionOrNull() is NullPointerException) } - 6
Kotlin collections and higher-order functions
Filter, map, group, sort, and fold lists with lambdas instead of loops.
How would you list the names sorted by age descending?
Reveal answer and reasoning
people.sortedByDescending { it.age }.map { it.name }.
Show starting example
Kotlindata class Person(val name: String, val age: Int, val city: String) fun main() { val people = listOf( Person("Ada", 36, "London"), Person("Bo", 25, "Paris"), Person("Cy", 41, "London"), Person("Di", 25, "Paris"), ) println(people.filter { it.age > 30 }.map { it.name }) println(people.groupBy { it.city }.mapValues { (_, list) -> list.size }) println(people.sumOf { it.age }) println(people.sortedWith(compareBy({ it.age }, { it.name })).map { it.name }) println(people.maxByOrNull { it.age }?.name) println(people.partition { it.city == "London" }.let { (london, others) -> london.size to others.size }) println((1..5).fold(1) { product, n -> product * n }) } - 7
Kotlin Classes
Declare a class with a primary constructor and private state.
What does account.deposit(-20) do here?
Reveal answer and reasoning
Nothing prevents it, so the balance drops to 130. Add a check inside deposit to reject negative amounts.
Show starting example
Kotlinclass Account(val owner: String, private var balance: Int) { fun deposit(amount: Int) { balance += amount } fun report(): String = "$owner has $balance" } fun main() { val account = Account("Ada", 100) account.deposit(50) println(account.report()) } - 8
Kotlin data classes and sealed interfaces
Model alternatives with a sealed hierarchy and let the compiler check that every case is handled.
What happens if you add object Triangle : Shape and do not update area?
Reveal answer and reasoning
The code stops compiling, because the when expression is no longer exhaustive.
Show starting example
Kotlinimport java.util.Locale sealed interface Shape data class Circle(val radius: Double) : Shape data class Rectangle(val width: Double, val height: Double) : Shape fun area(shape: Shape): Double = when (shape) { is Circle -> Math.PI * shape.radius * shape.radius is Rectangle -> shape.width * shape.height } fun main() { val shapes = listOf(Circle(1.5), Rectangle(2.0, 3.5)) for (shape in shapes) { println(String.format(Locale.ROOT, "%s -> %.2f", shape, area(shape))) } val circle = Circle(2.0) val bigger = circle.copy(radius = 3.0) println(circle == Circle(2.0)) println(bigger) val (width, height) = Rectangle(4.0, 5.0) println(width * height) } - 9
Kotlin object, companion object, and delegation
Create singletons, class-level factories with private constructors, and reuse behaviour by delegating to an interface.
How would you test Service without printing?
Reveal answer and reasoning
Pass a fake Logger that stores messages in a list, then check the list. That is possible because Service depends on the Logger interface.
Show starting example
Kotlininterface Logger { fun log(message: String) } class ConsoleLogger : Logger { override fun log(message: String) = println("LOG: $message") } class Service(logger: Logger) : Logger by logger { fun run() { log("service started") } } class Counter private constructor(val id: Int) { companion object { private var next = 1 fun create(): Counter = Counter(next++) } } object Config { val name = "demo" fun describe() = "config $name" } fun main() { Service(ConsoleLogger()).run() println(Counter.create().id) println(Counter.create().id) println(Config.describe()) } - 10
Kotlin sorting and binary search
Sort lists with sorted and sortedBy, and find a value or its insertion point with binarySearch.
How do you sort people by age and then name?
Reveal answer and reasoning
people.sortedWith(compareBy({ it.second }, { it.first })), comparing age first and name second.
Show starting example
Kotlinfun main() { val numbers = listOf(5, 2, 9, 1, 5, 6) println(numbers.sorted()) println(numbers.sortedDescending()) val sorted = numbers.sorted() println(sorted.binarySearch(9)) val missing = sorted.binarySearch(4) println("4 would be inserted at ${-missing - 1}") val words = listOf("pear", "apple", "Fig") println(words.sorted()) println(words.sortedBy { it.lowercase() }) val people = listOf("Ada" to 30, "Bo" to 25, "Cy" to 30, "Di" to 25) println(people.sortedBy { it.second }.map { it.first }) } - 11
Kotlin ArrayDeque as a stack and a queue
Check balanced brackets with a stack and serve items in arrival order with a queue.
What does balanced("{[()]}") return?
Reveal answer and reasoning
true: each closing bracket matches the most recent unmatched opener.
Show starting example
Kotlinfun balanced(text: String): Boolean { val stack = ArrayDeque<Char>() val pairs = mapOf(')' to '(', ']' to '[', '}' to '{') for (ch in text) { when (ch) { '(', '[', '{' -> stack.addLast(ch) ')', ']', '}' -> if (stack.removeLastOrNull() != pairs[ch]) return false } } return stack.isEmpty() } fun main() { for (test in listOf("(a[b]{c})", "(]", "((", ")(", "")) { println("'$test' -> ${balanced(test)}") } val queue = ArrayDeque<String>() queue.addLast("Ada") queue.addLast("Bo") queue.addLast("Cy") val served = queue.removeFirst() println("$served served, ${queue.size} waiting, next is ${queue.first()}") } - 12
Kotlin maps: counting and indexing
Count words with groupingBy and build a lookup map with getOrPut.
How would you find the most frequent word?
Reveal answer and reasoning
freq.maxByOrNull { it.value }?.key; with ties, it returns the first one encountered.
Show starting example
Kotlinfun main() { val text = "the cat and the hat and the bat" val freq = text.split(" ").groupingBy { it }.eachCount() println(freq.entries.sortedWith(compareBy({ -it.value }, { it.key })).joinToString(" ") { "${it.key}=${it.value}" }) val index = mutableMapOf<Char, MutableList<String>>() for (word in listOf("apple", "avocado", "banana", "blueberry", "cherry")) { index.getOrPut(word[0]) { mutableListOf() }.add(word) } println(index) println(index['b']?.size ?: 0) println(index['z']?.size ?: 0) } - 13
Kotlin unit tests with lambdas and runCatching
Write a minimal test function that reports passes and failures, including expected exceptions.
How would you add a boundary test for 0?
Reveal answer and reasoning
test("accepts the boundary 0") { check(percent("0") == 0) } because 0 is the lowest allowed value.
Show starting example
Kotlinfun percent(text: String): Int { val value = text.toIntOrNull() ?: throw NumberFormatException("not a number: $text") require(value in 0..100) { "out of range: $value" } return value } var failures = 0 fun test(name: String, body: () -> Unit) { val outcome = runCatching(body) if (outcome.isSuccess) { println("PASS $name") } else { failures++ println("FAIL $name: ${outcome.exceptionOrNull()?.message}") } } inline fun <reified E : Throwable> assertThrows(code: () -> Unit) { val error = runCatching(code).exceptionOrNull() check(error is E) { "expected ${E::class.simpleName} but got ${error?.let { it::class.simpleName }}" } } fun main() { test("parses 42") { check(percent("42") == 42) } test("accepts the boundary 100") { check(percent("100") == 100) } test("rejects 101") { assertThrows<IllegalArgumentException> { percent("101") } } test("rejects text") { assertThrows<NumberFormatException> { percent("abc") } } test("a wrong expectation") { check(percent("7") == 8) } println("failures: $failures") } - 14
Kotlin SHA-256 and PBKDF2 on the JVM
Hash text with MessageDigest and derive a salted password hash with PBKDF2.
Why store the salt next to the hash?
Reveal answer and reasoning
The salt is not secret. Its job is to make each hash unique, and it is needed again to verify the password at login.
Show starting example
Kotlinimport java.security.MessageDigest import java.security.SecureRandom import javax.crypto.SecretKeyFactory import javax.crypto.spec.PBEKeySpec fun hex(bytes: ByteArray): String = bytes.joinToString("") { "%02x".format(it) } fun derive(password: String, salt: ByteArray): ByteArray { val spec = PBEKeySpec(password.toCharArray(), salt, 600_000, 256) return SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256").generateSecret(spec).encoded } fun main() { val sha = MessageDigest.getInstance("SHA-256") println(hex(sha.digest("abc".toByteArray(Charsets.UTF_8)))) println(hex(sha.digest("abd".toByteArray(Charsets.UTF_8)))) val salt = ByteArray(16).also { SecureRandom().nextBytes(it) } val stored = derive("correct horse", salt) println("hash length: ${stored.size} bytes") println("right password: ${MessageDigest.isEqual(stored, derive("correct horse", salt))}") println("wrong password: ${MessageDigest.isEqual(stored, derive("wrong guess", salt))}") } - 15
Kotlin threads, AtomicInteger, and an executor
Start threads that update a shared counter safely and split a sum across a thread pool.
What would happen with a plain var counter instead?
Reveal answer and reasoning
The final value would usually be less than 200000, and different on every run, because increments from different threads overwrite each other.
Show starting example
Kotlinimport java.util.concurrent.Callable import java.util.concurrent.Executors import java.util.concurrent.atomic.AtomicInteger import kotlin.concurrent.thread fun main() { val counter = AtomicInteger(0) val threads = List(4) { thread { repeat(50_000) { counter.incrementAndGet() } } } threads.forEach { it.join() } println("counter = ${counter.get()}") val pool = Executors.newFixedThreadPool(4) val futures = (1..4).map { part -> pool.submit(Callable { ((part - 1) * 250 + 1..part * 250).sumOf { it.toLong() } }) } println("sum = ${futures.sumOf { it.get() }}") pool.shutdown() }