Extension Functions Workbook
Ten short exercises on Kotlin extensions — declaring extension functions and properties, the receiver, static resolution, nullable receivers, and members winning over extensions.
Practice problems for Extension Functions: Add Methods to Classes You Don’t Own. Each takes a minute or two. Most exercises auto-check: implement the extension 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 server. The resolution and nullable-receiver observation exercises 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.
declaring extensions
1. Extend String
Write an extension shout() on String that returns the uppercased text with a ! appended.
import org.junit.Test
import org.junit.Assert
class Test {
@Test fun shouts() {
Assert.assertEquals("uppercased with a bang", "HELLO!", "hello".shout())
}
}
//sampleStart
// TODO: return the uppercased receiver with a "!" appended
fun String.shout(): String = this
//sampleEnd Show answer Hide answer
fun String.shout() = uppercase() + "!"
"hello".shout() // "HELLO!"this (the receiver) is implicit, so uppercase() reads like a member call.
2. Extend Int
Write an extension isEven() on Int returning whether it’s even.
import org.junit.Test
import org.junit.Assert
class Test {
@Test fun evens() {
Assert.assertTrue("4 is even", 4.isEven())
Assert.assertFalse("7 is odd", 7.isEven())
}
}
//sampleStart
fun Int.isEven(): Boolean = false // TODO: return whether this is even
//sampleEnd Show answer Hide answer
fun Int.isEven() = this % 2 == 0
4.isEven() // true 3. Extend a generic type
Write List<Int>.secondOrNull() that returns the second element, or null if the list is too short.
import org.junit.Test
import org.junit.Assert
class Test {
@Test fun second() {
Assert.assertEquals("the second element", 20, listOf(10, 20, 30).secondOrNull())
Assert.assertEquals("null when too short", null, listOf(10).secondOrNull())
}
}
//sampleStart
fun List<Int>.secondOrNull(): Int? = null // TODO: 2nd element, or null if too short
//sampleEnd Show answer Hide answer
fun List<Int>.secondOrNull(): Int? = if (size >= 2) this[1] else null properties and nullable receivers
4. An extension property
Write a read-only extension property firstWord on String returning everything before the first space.
import org.junit.Test
import org.junit.Assert
class Test {
@Test fun firstWords() {
Assert.assertEquals("text before the first space", "hello", "hello world".firstWord)
}
}
//sampleStart
// TODO: return everything before the first space
val String.firstWord: String
get() = this
//sampleEnd Show answer Hide answer
val String.firstWord: String
get() = substringBefore(" ")
"hello world".firstWord // "hello"An extension property has no backing field, so it must compute its value in a getter.
5. A nullable receiver
Write emptyIfNull() on String? that returns "" when the receiver is null, otherwise the string itself.
fun main() {
fun String?.emptyIfNull(): String = "TODO" // TODO: "" when null, else the string itself
val name: String? = null
println("[" + name.emptyIfNull() + "]")
} Show answer Hide answer
fun String?.emptyIfNull(): String = this ?: ""
val name: String? = null
name.emptyIfNull() // ""Extending a nullable type lets the extension handle null itself — the call is safe with no ?.. (The standard library already ships this one as orEmpty().)
resolution rules
6. Make it polymorphic
Extensions resolve statically, so with fun Animal.speak() and fun Dog.speak(), a val pet: Animal = Dog() calls the Animal one. Rewrite speak as member functions so pet.speak() returns "woof".
import org.junit.Test
import org.junit.Assert
class Test {
@Test fun overrides() {
val pet: Animal = Dog()
Assert.assertEquals("Dog overrides speak", "woof", pet.speak())
}
}
//sampleStart
open class Animal { open fun speak() = "generic" }
// TODO: override speak() in Dog to return "woof"
class Dog : Animal()
//sampleEnd Show answer Hide answer
open class Animal { open fun speak() = "generic" }
class Dog : Animal() { override fun speak() = "woof" }
val pet: Animal = Dog()
pet.speak() // "woof" — members dispatch on the runtime type 7. Members win
Write a class Box with a member size() returning 1, plus an extension Box.size() returning 99. What does Box().size() return?
fun main() {
class Box { fun size() = 1 }
fun Box.size() = 99
// TODO: predict the result before you run — does the member or the extension win?
println(Box().size())
} Show answer Hide answer
class Box { fun size() = 1 }
fun Box.size() = 99
Box().size() // 1 — a member always wins over an extension 8. Chain reads top-to-bottom
Rewrite reversedText(trimmed(input)) (two hypothetical helpers) as a readable extension chain input.trimmed().reversedText() — just write the two extension signatures that make it possible.
fun main() {
// TODO: implement trimmed() and reversedText() so the chain below reads left-to-right
fun String.trimmed() = this
fun String.reversedText() = this
val input = " hello "
println("[" + input.trimmed().reversedText() + "]")
} Show answer Hide answer
fun String.trimmed() = trim()
fun String.reversedText() = reversed()Extensions let calls read in subject-verb order instead of inside-out.
9. Extend with a parameter
Write an extension repeatTimes(n: Int) on String returning the string concatenated n times.
import org.junit.Test
import org.junit.Assert
class Test {
@Test fun repeats() {
Assert.assertEquals("string repeated n times", "ababab", "ab".repeatTimes(3))
Assert.assertEquals("zero times is empty", "", "ab".repeatTimes(0))
}
}
//sampleStart
fun String.repeatTimes(n: Int): String = this // TODO: return the string concatenated n times
//sampleEnd Show answer Hide answer
fun String.repeatTimes(n: Int) = repeat(n) 10. Nullable receiver, used safely
Given name: String?, call your emptyIfNull() extension on it directly. Why is no ?. needed?
fun main() {
fun String?.emptyIfNull(): String = this ?: ""
val name: String? = null
val result = "" // TODO: call emptyIfNull() on name directly — no ?. needed
println("[" + result + "]")
} Show answer Hide answer
name.emptyIfNull()Because the receiver type is String?, the extension itself accepts null — so the plain dot call is safe.
Going deeper: static dispatch and nullable receivers
12. Which speak() runs?
Given Animal.speak() returning "generic" and Dog.speak() returning "woof", what does this print, and why?
open class Animal
class Dog : Animal()
fun Animal.speak() = "generic"
fun Dog.speak() = "woof"
fun main() {
val pet: Animal = Dog()
// TODO: predict before running
println(pet.speak())
} Show answer Hide answer
generic. Extensions are dispatched statically, by the declared type of the variable (Animal), not the runtime type (Dog). A real overridden member would print woof; an extension picks by the type the compiler sees, which is why extensions are the wrong tool for polymorphism.
13. Handle null inside the extension
Write an extension orDash on String? that returns the string itself, or "-" when it’s null — callable without a safe call.
import org.junit.Test
import org.junit.Assert
class Test {
@Test fun orDash() {
Assert.assertEquals("Ada", "Ada".orDash())
val nothing: String? = null
Assert.assertEquals("-", nothing.orDash())
}
}
//sampleStart
fun String?.orDash(): String =
"" // TODO: this, or "-" when null
//sampleEnd Show answer Hide answer
fun String?.orDash(): String = this ?: "-"Because the receiver type is String?, the null check lives inside the extension, so callers need no ?. — the same trick behind the standard library’s orEmpty().
Back to the lesson, Extension Functions, or on to the next one: equality.
Comments