Free Handbook · Every example compiled & verified

Generics & Extensions

Generic classes and functions, in/out variance, reified types, extension functions, scope functions, operators and delegation with by, lazy and observable.

0 / 136 lessons🔥 0 day streak
ShareXLinkedIn

Module 09 · what you'll be able to do

  • Write generic classes and functions with upper bounds, and read variance (in, out, *) in library signatures
  • Use a reified type parameter to filter or check by type at runtime
  • Add behaviour to types you do not own with extension functions and properties
  • Pick the right scope function (let, run, with, apply, also) by what it returns and how it names the object
  • Overload operators, write infix functions, and delegate with by, lazy and Delegates.observable
01

Generic classes and functions

A generic type has a placeholder, conventionally T, that the caller fills in. List<String> and List<Int> are the same class with different type arguments, and the compiler checks every use. Kotlin usually infers the argument, so you rarely write it out.

kotlinMain.kt
class Stack<T> {
    private val items = mutableListOf<T>()
    fun push(item: T) { items.add(item) }
    fun pop(): T? = items.removeLastOrNull()
    val size: Int get() = items.size
}

fun <T> firstOrDefault(xs: List<T>, default: T): T = if (xs.isEmpty()) default else xs[0]

fun main() {
    val s = Stack<String>()
    s.push("a"); s.push("b")
    println(s.pop())
    println(s.size)

    println(firstOrDefault(listOf(3, 4), 0))
    println(firstOrDefault(emptyList(), "none"))
}
Outputcompiled & run with real Kotlin
b
1
3
none
Your turn

Add a peek(): T? that returns the top item without removing it.

An upper bound (<T : Comparable<T>>) restricts which types are allowed, and in return lets you call the bound's methods on T. For several bounds, use a where clause.

kotlinMain.kt
fun <T : Comparable<T>> largest(xs: List<T>): T {
    var best = xs[0]
    for (x in xs) if (x > best) best = x
    return best
}

fun <T> describe(x: T): String where T : CharSequence, T : Comparable<T> =
    "$x has ${x.length} chars"

fun main() {
    println(largest(listOf(3, 9, 4)))
    println(largest(listOf("pear", "apple", "zucchini")))
    println(describe("kotlin"))
}
Outputcompiled & run with real Kotlin
9
zucchini
kotlin has 6 chars
Error you will hit

A type that does not satisfy the bound

kotlin
fun <T : Comparable<T>> maxOf3(a: T, b: T, c: T): T = maxOf(a, maxOf(b, c))

class Box(val v: Int)

fun main() = println(maxOf3(Box(1), Box(2), Box(3)))
Main.kt:5:22: error: cannot infer type for type parameter 'T'. Specify it explicitly.
fun main() = println(maxOf3(Box(1), Box(2), Box(3)))
                     ^^^^^^
Main.kt:5:29: error: argument type mismatch: actual type is 'Box', but 'Comparable<uninferred T (of fun <T : Comparable<T>> maxOf3)>' was expected.
fun main() = println(maxOf3(Box(1), Box(2), Box(3)))
                            ^^^^^^
Why the compiler said that

Box does not implement Comparable<Box>, so the compiler cannot promise that maxOf can compare two of them.

The fix

Implement Comparable on the class, or pass a comparator-based function such as maxOf(a, b, c, compareBy { it.v })... or simply compare the property you care about.

kotlin
fun <T : Comparable<T>> maxOf3(a: T, b: T, c: T): T = maxOf(a, maxOf(b, c))

class Box(val v: Int) : Comparable<Box> {
    override fun compareTo(other: Box) = v.compareTo(other.v)
}

fun main() = println(maxOf3(Box(1), Box(2), Box(3)).v)
02

Variance: in, out and star projection

Is a List<String> a List<Any>? For a read-only list, yes: anything you read out of it is a String, which is an Any. For a MutableList, no: if it were, someone could add an Int to your list of strings. Kotlin writes this down at the declaration: out T means the class only produces T (covariant), in T means it only consumes T (contravariant). A plain T is invariant.

kotlinMain.kt
interface Source<out T> { fun next(): T }
interface Sink<in T> { fun put(item: T) }

class Numbers : Source<Int> { private var n = 0; override fun next() = ++n }
class Printer : Sink<Any> { override fun put(item: Any) = println("got $item") }

fun main() {
    val src: Source<Number> = Numbers()   // out: Source<Int> is a Source<Number>
    val sink: Sink<String> = Printer()    // in: Sink<Any> is a Sink<String>
    println(src.next())
    sink.put("hello")

    val strings: List<String> = listOf("a")
    val anys: List<Any> = strings          // List is declared List<out E>
    println(anys)
}
Outputcompiled & run with real Kotlin
1
got hello
[a]

Producers can be widened, consumers can be narrowed.

Error you will hit

MutableList is invariant

kotlin
fun main() {
    val names: MutableList<Any> = mutableListOf<String>("a", "b")
    names.add(42)
}
Main.kt:2:33: error: initializer type mismatch: expected 'MutableList<Any>', actual 'MutableList<String>'.
    val names: MutableList<Any> = mutableListOf<String>("a", "b")
                                ^
Why the compiler said that

If this compiled, names.add(42) would put an Int into a list the rest of the program believes holds only Strings. MutableList<E> both reads and writes E, so it cannot be covariant.

The fix

Declare the list with the type you actually want to store, or accept a read-only List<Any> if you only read.

kotlin
fun main() {
    val names: MutableList<Any> = mutableListOf("a", "b")
    names.add(42)
}
Error you will hit

Using an out type parameter as input

kotlin
class Box<out T>(private var item: T) {
    fun get(): T = item
    fun set(value: T) { item = value }
}

fun main() = println(Box("x").get())
Main.kt:3:20: error: type parameter 'T' is declared as 'out' but occurs in 'in' position in type 'T (of class Box<out T>)'.
    fun set(value: T) { item = value }
                   ^
Why the compiler said that

By writing out T you promised the class only hands T out. A set(value: T) parameter takes T in, which would break that promise.

The fix

Drop out (make it invariant) if the class really must accept T, or remove the setter.

kotlin
class Box<T>(private var item: T) {
    fun get(): T = item
    fun set(value: T) { item = value }
}

Use-site variance does the same thing for one parameter: fun copy(from: Array<out Any>, to: Array<Any>). A star projection List<*> means "a list of some type I do not know" — you can read items as Any? but cannot add anything.

kotlinMain.kt
fun printAll(xs: List<*>) {
    for (x in xs) print("${x ?: "null"} ")
    println("(${xs.size})")
}

fun main() {
    printAll(listOf(1, 2))
    printAll(listOf("a", null))
}
Outputcompiled & run with real Kotlin
1 2 (2)
a null (2)
03

Reified type parameters

On the JVM, type arguments are erased at runtime: a List<String> is just a List. So inside an ordinary generic function you cannot write x is T. An inline function is copied into each call site, so the compiler knows the real type there — mark the parameter reified and T becomes usable at runtime. That is how filterIsInstance<String>() works.

kotlinMain.kt
inline fun <reified T> List<Any>.only(): List<T> = filter { it is T }.map { it as T }

inline fun <reified T> typeName(): String = T::class.simpleName ?: "?"

fun main() {
    val mixed: List<Any> = listOf(1, "two", 3.0, "four", 5)
    println(mixed.only<String>())
    println(mixed.only<Int>())
    println(mixed.filterIsInstance<Double>())
    println(typeName<Map<String, Int>>())
}
Outputcompiled & run with real Kotlin
[two, four]
[1, 5]
[3.0]
Map
Error you will hit

reified without inline

kotlin
fun <reified T> isType(x: Any): Boolean = x is T

fun main() = println(isType<String>("hi"))
Main.kt:1:6: error: only type parameters of inline functions can be reified.
fun <reified T> isType(x: Any): Boolean = x is T
     ^^^^^^^
Why the compiler said that

Only an inline function is expanded at each call site, which is what gives the compiler a concrete type to substitute. A normal function is compiled once for every T, and T is erased.

The fix

Add inline.

kotlin
inline fun <reified T> isType(x: Any): Boolean = x is T

fun main() = println(isType<String>("hi"))
04

Extension functions and properties

An extension function adds a method to a type you do not own — String, List, a library class — without inheritance. Inside, this is the receiver. It is resolved statically: the compiler turns "x".shout() into a plain call shout("x"), so extensions cannot see private members and cannot be overridden.

kotlinMain.kt
fun String.shout(): String = uppercase() + "!"

fun List<Int>.secondOrNull(): Int? = if (size >= 2) this[1] else null

val String.wordCount: Int
    get() = trim().split(Regex("\\s+")).count { it.isNotEmpty() }

fun String?.orDash(): String = if (this.isNullOrBlank()) "-" else this

fun main() {
    println("hello".shout())
    println(listOf(4, 8, 15).secondOrNull())
    println("  the quick brown fox ".wordCount)
    val missing: String? = null
    println(missing.orDash())
}
Outputcompiled & run with real Kotlin
HELLO!
8
4
-
Your turn

Write fun Int.isEven(): Boolean and print 7.isEven().

Members win, and dispatch is static
If a class already has a member with the same signature, the member is always called and your extension is ignored (the compiler warns). And because extensions are resolved by the declared type, val a: Animal = Dog() calls the Animal extension, not the Dog one.
05

Scope functions: let, run, with, apply, also

The five scope functions all run a block with an object in scope. They differ in two ways only: how the block refers to the object (it or this) and what the call returns (the object itself, or the block's last expression).

FunctionObject isReturnsTypical use
letitblock resultx?.let { … } — run only when not null; transform
runthisblock resultconfigure and compute a result
with(x)thisblock resultcall many methods on one object
applythisthe objectbuild/configure an object
alsoitthe objectside effects: logging, validation
kotlinMain.kt
class Request { var url = ""; var retries = 0; val headers = mutableMapOf<String, String>() }

fun main() {
    val req = Request().apply {
        url = "https://api.example.com"
        retries = 3
        headers["Accept"] = "json"
    }.also { println("built ${it.url}") }

    val summary = with(req) { "$url retries=$retries headers=${headers.size}" }
    println(summary)

    val token: String? = "abc123"
    val len = token?.let { it.length } ?: 0
    println(len)

    val host = req.run { url.removePrefix("https://").substringBefore("/") }
    println(host)
}
Outputcompiled & run with real Kotlin
built https://api.example.com
https://api.example.com retries=3 headers=1
6
api.example.com
Visualizeapply returns the object, let returns the resultStep 1 / 5
val list = mutableListOf(1, 2)
val a = list.apply { add(3) }
val b = list.let { it.size * 10 }
val c = list.also { it.add(4) }
println("$a $b $c")
Line 1

A mutable list with two items.

Variables now
list[1, 2]
All 5 steps as a table
StepLineWhat happenedVariables now
11A mutable list with two items.list = [1, 2]
22apply runs add(3) with the list as this, then returns the list itself. a is the same object as list.list = [1, 2, 3] a = same as list
33let returns the lambda result, not the list.b = 30
44also runs the side effect and returns the list. All three variables now point at one list with four items.list = [1, 2, 3, 4] c = same as list
55a is printed after the add on line 4, so it shows four items too.
Readability beats cleverness
Chains like x?.let { … }?.also { … }?.run { … } are hard to review. In real code, apply for building objects and ?.let for null checks cover most needs; plain if and local variables cover the rest.
06

Infix functions and operator overloading

A one-parameter member or extension marked infix can be called without dot and parentheses — that is all 1 to "one" is. Marking a function with a fixed name as operator lets you use the matching symbol: plus for +, times for *, get for [], contains for in, compareTo for <, unaryMinus for -x.

kotlinMain.kt
data class Money(val cents: Long) {
    operator fun plus(other: Money) = Money(cents + other.cents)
    operator fun times(n: Int) = Money(cents * n)
    operator fun compareTo(other: Money) = cents.compareTo(other.cents)
    override fun toString() = "$" + (cents / 100) + "." + (cents % 100).toString().padStart(2, '0')
}

class Grid(private val w: Int) {
    private val cells = IntArray(w * w)
    operator fun get(r: Int, c: Int) = cells[r * w + c]
    operator fun set(r: Int, c: Int, v: Int) { cells[r * w + c] = v }
}

infix fun Int.percentOf(total: Int): Int = total * this / 100

fun main() {
    val price = Money(1999)
    println(price * 2 + Money(1))
    println(price > Money(1000))
    val g = Grid(3)
    g[1, 2] = 7
    println(g[1, 2])
    println(20 percentOf 250)
}
Outputcompiled & run with real Kotlin
$39.99
true
7
50
07

Delegation with by, lazy and observable

Class delegation — class Logged(inner: Store) : Store by inner — generates every interface method as a call to inner, and you override only the ones you want to change. This is composition without the boilerplate. Property delegation hands a property's get/set to another object: by lazy { } computes the value on first access and caches it; Delegates.observable runs a callback on each change; by map reads properties from a map.

kotlinMain.kt
interface Store { fun get(k: String): String?; fun put(k: String, v: String) }

class MemoryStore : Store {
    private val m = mutableMapOf<String, String>()
    override fun get(k: String) = m[k]
    override fun put(k: String, v: String) { m[k] = v }
}

class LoggingStore(private val inner: Store) : Store by inner {
    override fun put(k: String, v: String) {
        println("put $k")
        inner.put(k, v)
    }
}

fun main() {
    val s: Store = LoggingStore(MemoryStore())
    s.put("lang", "kotlin")
    println(s.get("lang"))   // not overridden: forwarded to inner
}
Outputcompiled & run with real Kotlin
put lang
kotlin
kotlinMain.kt
import kotlin.properties.Delegates

class Settings(map: Map<String, Any>) {
    val config: String by lazy {
        println("loading config...")
        "dark-mode"
    }
    var volume: Int by Delegates.observable(5) { _, old, new ->
        println("volume $old -> $new")
    }
    val user: String by map
}

fun main() {
    val s = Settings(mapOf("user" to "ada"))
    println("created")
    println(s.config)
    println(s.config)
    s.volume = 8
    println(s.user)
}
Outputcompiled & run with real Kotlin
created
loading config...
dark-mode
dark-mode
volume 5 -> 8
ada

The lazy block runs once, on first access, not at construction.

type parameter
A placeholder such as T in class Box<T>, filled in by each use.
upper bound
<T : Comparable<T>> — restricts T and lets you call the bound's members.
covariance (out)
A type that only produces T; Source<Int> can be used as Source<Number>.
contravariance (in)
A type that only consumes T; Sink<Any> can be used as Sink<String>.
star projection
List<*> — a list of some unknown type; safe to read as Any?, not to write.
type erasure
The JVM drops generic type arguments at runtime, which is why x is T needs reified.
reified
A type parameter of an inline function that is available at runtime.
extension function
A function declared with a receiver type, called like a member but resolved statically.
scope function
One of let, run, with, apply, also — runs a block with an object in scope.
delegation (by)
Forwarding an interface's methods, or a property's get/set, to another object.
Quick check

Which scope function returns the object itself and refers to it as it inside the block?

Quick check

Why can List<String> be assigned to List<Any> but MutableList<String> cannot be assigned to MutableList<Any>?

Frequently asked questions

What is the difference between in and out in Kotlin generics?
out T marks a type that only returns T (a producer), so it can be treated as a supertype: a Source of Int is a Source of Number. in T marks a type that only accepts T (a consumer), so it works the other way round: a Comparator of Any can compare Strings.
When should I use apply vs also vs let in Kotlin?
Use apply to configure an object you are building, also for a side effect such as logging that should not change the chain, and ?.let to run code only when a value is not null or to transform a value into something else.
Do extension functions modify the original class?
No. An extension is compiled to a static function that takes the receiver as its first argument. The class is unchanged, private members stay private, and the extension is only visible where it is imported.

Finish the Kotlin handbook, then get hired

Sit the exam for your certificate, run your resume through the ATS checker, and see the jobs that ask for exactly this.

Check my resume
Found this course useful? Share it.
ShareXLinkedIn

Comments

0

Join the conversation. Sign in to leave a comment — we'd love to hear your thoughts.