Kotlin Cheatsheet

Generics & Extensions

Use this Kotlin reference while you build software engineering projects, review code for technical interview prep, or polish examples for a software engineer resume.

Generic functions & classes

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

class Box<T>(val value: T)
class Cache<K, V>(private val map: MutableMap<K, V> = mutableMapOf()) {
    operator fun get(k: K): V? = map[k]
    operator fun set(k: K, v: V) { map[k] = v }
}

Constraints:

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

fun <T> pickTop(items: List<T>) where T : Comparable<T>, T : Cloneable = items.max()

Variance: in / out / star

ModifierMeaningExample
out Tcovariant — T only produced (return positions)List<out E>, Producer<out T>
in Tcontravariant — T only consumed (param positions)Comparator<in T>
invariantdefault — exact type requiredMutableList<T>
*star projection — unknown argumentfun dump(xs: List<*>)
val nums: List<Number> = listOf(1, 2)          // ok: List is `out` (read-only)
// val m: MutableList<Number> = mutableListOf(1) // error: MutableList invariant

fun copyInto(dst: MutableList<in Int>, src: List<Int>) { dst.addAll(src) }

Use-site variance also works: fun fill(dest: Array<in String>).

reified type parameters

Generics are erased at runtime — unless the function is inline with reified T, which keeps the real type at the call site:

inline fun <reified T> Any?.isA(): Boolean = this is T

inline fun <reified T> Json.parse(s: String): T = decodeFromString(s)

val user: User = json.parse(body)      // no Class<T> parameter needed
"str".isA<String>()                    // true
filterIsInstance<Circle>()             // stdlib example of reified in action

Extension functions & properties

fun String.isPalindrome(): Boolean = this == reversed()
fun <T> List<T>.penultimate(): T = this[size - 2]

val String.wordCount: Int                       // extension property (no backing field)
    get() = trim().split(Regex("\\s+")).size

fun StringBuilder?.orEmpty(): String = this?.toString() ?: ""   // nullable receiver

"level".isPalindrome()
"one two three".wordCount

Extensions are resolved statically (by the declared type, not runtime type) and cannot override members. Scope them: extensions declared inside a class or imported explicitly keep namespaces clean.

Operator overloading

OperatorFunction nameOperatorFunction name
a + bplusa[i]get / set
a - bminusa in bcontains
a * btimesa()invoke
a += bplusAssigna..brangeTo
-aunaryMinusa < bcompareTo
data class Vec(val x: Int, val y: Int) {
    operator fun plus(o: Vec) = Vec(x + o.x, y + o.y)
    operator fun times(k: Int) = Vec(x * k, y * k)
}
val v = Vec(1, 2) + Vec(3, 4) * 2

Comparators & sorting

data class Job(val start: Int, val end: Int, val pay: Int)

jobs.sortedWith(compareBy({ it.end }, { it.start }))
jobs.sortedWith(compareByDescending<Job> { it.pay }.thenBy { it.start })
jobs.maxWith(compareBy { it.pay })
val cmp: Comparator<Job> = compareBy { it.end }
mutable.sortBy { it.end }                // in-place variants: sortBy/sortWith/sort

typealias

typealias Grid = Array<IntArray>
typealias Handler = (Request) -> Response
typealias UserId = String                // readability only — not a new type

fun route(h: Handler) { }

For a real distinct type with near-zero overhead, use a value class: @JvmInline value class UserId(val raw: String).