Skip to content

Kotlin Spickzettel

Moderne JVM-Sprache, prägnant und vollständig Java-interoperabel.

01

Grundlagen

Variablen & Typen

Bevorzugen Sie val (unveränderlich) über var (veränderlich). Kotlin leitet Typen ab, erlaubt aber explizite Annotationen. Verwenden Sie 'is' für Typ-Prüfungen (smart-castet automatisch). Alles ist ein Objekt – keine Primitives in Kotlin-Syntax.

kotlin
val name = "Alice"   // immutable (preferred)
var age = 30          // mutable
val pi: Double = 3.14159
val isDev: Boolean = true
val nums: List<Int> = listOf(1, 2, 3)
println(name::class)  // class kotlin.String
println(age is Int)   // true

String-Templates

$var für einfache Variablen, ${expr} für Ausdrücke. Triple-Quoted-Strings bewahren Zeilenumbrüche – verwenden Sie trimIndent() für saubere Formatierung. String-Templates machen String-Erstellung prägnant und lesbar ohne Format-Strings.

kotlin
val name = "Alice"
val age = 30
println("Name: $name, Age: $age")  // simple var
println("Length: ${name.length}")  // expression
println("Upper: ${name.uppercase()}")
val multiline = """
  Hello, $name
  Age: $age
""".trimIndent()

Null-Sicherheit

Kotlins Null-Sicherheit: ? markiert Nullable-Typen, ?. Safe-Call (gibt null bei null), !! assertet Non-Null (wirft NPE bei null), ?: Elvis (Standard bei null). Dies eliminiert NullPointerException zur Kompilierzeit – Core-Kotlin-Feature.

kotlin
var name: String? = null  // nullable type
println(name?.length)     // null (safe call)
name = "Alice"
println(name!!.length)    // 5 (non-null assertion)
val len: Int = name?.length ?: 0  // elvis operator
// Non-null types can never be null
val s: String = "hi"  // not nullable

Typumwandlung

Kotlin erfordert explizite Typumwandlung (kein implizites Widening wie Java). Verwenden Sie toInt()/toDouble()/toString() für Konvertierung. toIntOrNull() gibt null bei Fehlschlag – sicherer als toInt(), das wirft. Behandeln Sie immer null aus Parsing.

kotlin
val n: Int = 42
val d: Double = n.toDouble()
val s: String = n.toString()
val fromStr: Int? = "100".toIntOrNull()
val invalid: Int? = "abc".toIntOrNull()  // null
println(d, s, fromStr, invalid)

Typ-Prüfungen & Smart Cast

is prüft Typ und smart-castet die Variable in diesem Zweig – kein expliziter Cast nötig. !is ist die Negation. Verwenden Sie as für unsichere Casts (wirft ClassCastException), as? für sichere Casts (gibt null bei Fehlschlag). Smart Cast ist ein wesentliches Kotlin-Convenience.

kotlin
fun describe(x: Any): String =
    when (x) {
        is Int -> "Int: ${x + 1}"  // x smart-cast to Int
        is String -> "String of length ${x.length}"
        is List<*> -> "List with ${x.size} items"
        else -> "Unknown"
    }
println(describe(42))      // Int: 43
println(describe("hello")) // String of length 5
02

Strings

Häufige String-Methoden

Kotlin-Strings haben reiche Erweiterungsfunktionen aus der Standardbibliothek. Die meisten geben neue Strings zurück (unveränderlich). Verwenden Sie diese statt manueller Schleifen. Funktionen wie uppercase() sind locale-abhängig – verwenden Sie uppercase(Locale.ROOT) für konsistente Ergebnisse.

kotlin
val s = "Hello, World"
println(s.length)          // 12
println(s.uppercase())     // HELLO, WORLD
println(s.lowercase())     // hello, world
println(s.split(", "))     // [Hello, World]
println(s.replace("o", "0"))  // Hell0, W0rld
println(s.reversed())      // dlroW ,olleH
println(s.startsWith("Hello"))  // true

Mehrzeilige & Raw Strings

Triple-Quoted-Strings sind raw – keine Escape-Sequenzen nötig (außer dreifache doppelte Anführungszeichen). trimMargin('|) behält nur Text nach | für saubere Einrückung. trimIndent() entfernt gemeinsame Whitespace. Ideal für SQL, JSON, HTML in Code.

kotlin
val text = """
    |Hello,
    |World!
""".trimMargin()  // | marks line start
println(text)
val json = """{"name": "Alice", "age": 30}"""
println(json)  // no escape needed for quotes
// trimIndent() removes common leading whitespace

String-Erstellung

joinToString ist der idiomatische Weg, Collections mit Separatoren, Präfix, Postfix zu verbinden. buildString bietet einen StringBuilder-Scope für prägnantes Erstellen. Vermeiden Sie wiederholtes + in Schleifen – verwenden Sie joinToString oder StringBuilder für Effizienz.

kotlin
val parts = listOf("apple", "banana", "cherry")
println(parts.joinToString(", "))  // apple, banana, cherry
println(parts.joinToString(prefix="[", postfix="]"))
val sb = StringBuilder()
for (p in parts) sb.append(p).append(" ")
println(sb.toString().trim())
// buildString for concise building
val result = buildString {
    parts.forEach { append(it).append(";") }
}

String zu Zahl

toInt()/toDouble() werfen NumberFormatException bei ungültiger Eingabe. toIntOrNull() gibt null – paaren Sie mit ?: für sichere Standardwerte. toBooleanStrict() akzeptiert nur 'true'/'false'. Verwenden Sie OrNull-Varianten für Benutzereingaben oder nicht vertrauenswürdige Daten.

kotlin
val n = "42".toInt()           // 42 (throws if invalid)
val d = "3.14".toDouble()      // 3.14
val safe = "abc".toIntOrNull() // null
val withDefault = "x".toIntOrNull() ?: 0  // 0
val bool = "true".toBooleanStrict()  // true
println(n, d, safe, withDefault)

Regex

Die Regex-Klasse wrapt Javas Pattern. find gibt MatchResult? (erster Match), findAll gibt alle. matches prüft vollen Match, containsMatchIn prüft partiell. Verwenden Sie Triple-Quoted-Strings für Patterns, um doppeltes Escaping von Backslashes zu vermeiden.

kotlin
val email = Regex("[\w.]+@[\w]+\.[a-z]+")
val text = "Contact: [email protected]"
val match = email.find(text)
println(match?.value)  // [email protected]
println(email.matches("[email protected]"))  // true
val replaced = text.replace(Regex("\d+"), "#")
println(email.containsMatchIn(text))  // true
03

Datenstrukturen

List

listOf erstellt unveränderliche Listen, mutableListOf für veränderliche. Listen sind null-indiziert. Verwenden Sie first()/last() für Enden (werfen bei leer), firstOrNull() für Sicherheit. contains prüft Mitgliedschaft. Bevorzugen Sie unveränderliche Listen, es sei denn, Sie müssen modifizieren.

kotlin
val nums = listOf(1, 2, 3)  // immutable
val mutable = mutableListOf(1, 2, 3)
mutable.add(4)
mutable[0] = 0
println(nums.size)         // 3
println(nums.first())      // 1
println(nums.last())       // 3
println(nums.contains(2))  // true
println(nums.indexOf(2))   // 1

Map

mapOf erstellt unveränderliche Maps, mutableMapOf für veränderliche. 'to' infix erstellt Paare. [] -Zugriff gibt nullable V? (null, wenn fehlt). Verwenden Sie getOrDefault oder getValue (wirft, wenn fehlt). Iterieren Sie mit Destrukturierung (k, v).

kotlin
val ages = mapOf("Alice" to 30, "Bob" to 25)
println(ages["Alice"])          // 30
println(ages.getOrDefault("Eve", 0))  // 0
println(ages.containsKey("Alice"))    // true
for ((k, v) in ages) println("$k: $v")
val mutable = mutableMapOf("a" to 1)
mutable["b"] = 2  // add/update

Set

setOf erstellt unveränderliche Sets, mutableSetOf für veränderliche. union/intersect/subtract geben neue Sets. Sets erzwingen Eindeutigkeit – add gibt false, wenn Element existiert. Verwenden Sie für Deduplizierung und Mengenoperationen. LinkedHashSet bewahrt Einfügereihenfolge.

kotlin
val a = setOf(1, 2, 3)
val b = setOf(3, 4, 5)
println(a union b)        // [1,2,3,4,5]
println(a intersect b)    // [3]
println(a subtract b)     // [1,2]
println(a.contains(2))    // true
val mutable = mutableSetOf(1, 2)
mutable.add(3)  // true if added

Array & Primitive Arrays

arrayOf erstellt Array<T> (geboxt für Primitives). Verwenden Sie intArrayOf/doubleArrayOf für ungeboxte Primitive Arrays (bessere Performance). Array(size) { init } erstellt mit einer Init-Funktion. Arrays sind veränderlich; bevorzugen Sie List für die meisten Anwendungsfälle.

kotlin
val arr = arrayOf(1, 2, 3)  // Array<Int>
arr[0] = 10
println(arr.size)  // 3
// Specialized arrays for primitives (no boxing)
val ints = intArrayOf(1, 2, 3)
val doubles = doubleArrayOf(1.0, 2.0)
// Array constructor
val squares = Array(5) { it * it }  // [0,1,4,9,16]
println(squares.toList())

Pair & Triple

Pair und Triple gruppieren 2-3 Werte. 'to' infix erstellt Pairs (häufig für Maps). Destrukturieren mit val (a, b) = pair. Für mehr als 3 Werte oder benannte Felder verwenden Sie Data-Klassen – sie sind klarer und wartbarer.

kotlin
val pair = "Alice" to 30  // Pair<String, Int>
println(pair.first)   // Alice
println(pair.second)  // 30
val (name, age) = pair  // destructuring
val triple = Triple(1, "a", 2.0)
println(triple.first, triple.third)
// Useful for returning two values
fun minMax(list: List<Int>): Pair<Int, Int> =
    Pair(list.min(), list.max())
04

Kontrollfluss

If / Else (Ausdruck)

In Kotlin ist if/else ein Ausdruck, der einen Wert zurückgibt. Dies ersetzt den ternären Operator. Beide Zweige müssen kompatible Typen zurückgeben. Verwenden Sie für prägnante bedingte Zuweisung. Als Anweisung (ohne else) gibt es Unit zurück.

kotlin
val score = 85
val grade = if (score >= 90) "A"
            else if (score >= 80) "B"
            else if (score >= 70) "C"
            else "F"
println(grade)  // B
// if returns a value—no ternary needed
// Both branches must have compatible types

When (Switch)

when ist Kotlins mächtiges switch – unterstützt Literale, Ranges (in), Negation (!in), mehrere Werte (Komma) und is-Prüfungen. Muss erschöpfend sein, wenn als Ausdruck mit Sealed-Typen verwendet. Ohne Subjekt ist es eine sauberere if/else-Kette.

kotlin
val n = 2
val label = when (n) {
    0 -> "zero"
    1, 2, 3 -> "small"
    in 4..10 -> "medium"
    !in 1..100 -> "out of range"
    else -> "large"
}
println(label)  // small
// When without subject = multi-condition if
when {
    n > 0 -> println("positive")
    n < 0 -> println("negative")
    else -> println("zero")
}

For-Schleifen & Ranges

.. ist inklusive Range, until ist exklusiv, downTo zählt abwärts, step setzt Inkrement. withIndex() gibt indizierte Iteration. for-in funktioniert mit jedem Iterable. Diese Ranges sind prägnant und lesbar für Zählschleifen.

kotlin
for (i in 0 until 5) print(i)  // 01234 (excludes 5)
for (i in 1..5) print(i)      // 12345 (includes 5)
for (i in 5 downTo 1) print(i)  // 54321
for (i in 1..10 step 2) print(i)  // 13579
val list = listOf("a", "b", "c")
for ((index, value) in list.withIndex()) {
    println("$index: $value")
}

While & Do-While

while prüft vorher, do-while prüft nachher (läuft mindestens einmal). Beide erfordern veränderlichen Zustand. Bevorzugen Sie for-Schleifen oder funktionale Operationen (map, filter) für Unveränderlichkeit. Verwenden Sie while für bedingungsgesteuerte Schleifen, wo die Anzahl nicht bekannt ist.

kotlin
var count = 0
while (count < 3) {
    println(count)
    count++
}
var j = 0
do {
    println(j)
    j++
} while (j < 3)
// do-while runs at least once
// Prefer for loops when possible

Break & Continue (Labels)

continue springt zur nächsten Iteration, break verlässt die Schleife. Labels (@outer) ermöglichen das Brechen verschachtelter Schleifen – definieren Sie mit label@ vor der Schleife, break/continue mit @label. Verwenden Sie sparsam; Refactoring zu Funktionen ist oft sauberer.

kotlin
for (i in 1..5) {
    if (i == 3) continue  // skip 3
    if (i == 5) break     // stop at 5
    println(i)  // 1, 2, 4
}
// Labeled breaks for nested loops
outer@ for (i in 1..3) {
    for (j in 1..3) {
        if (i == 2 && j == 2) break@outer
        println("$i,$j")
    }
}
05

Funktionen & Lambdas

Funktionsdefinition

Funktionen verwenden fun name(params): ReturnType. Ausdrucks-Body (= expr) für einzelne Ausdrücke. Default-Params und benannte Argumente unterstützt. Unit-Rückgabetyp = nur Nebeneffekt. Default-Params reduzieren die Notwendigkeit für Overloading.

kotlin
fun add(a: Int, b: Int): Int = a + b  // expression body
fun greet(name: String, greeting: String = "Hello"): String {
    return "$greeting, $name!"
}
println(add(3, 4))           // 7
println(greet("Alice"))      // Hello, Alice!
println(greet("Bob", greeting = "Hi"))  // named arg
fun log(msg: String): Unit = println(msg)  // Unit = void

Lambdas & Higher-Order

Lambdas: { params -> body }. 'it' ist Shorthand für einen einzelnen Parameter. Geben Sie Lambdas an Higher-Order Functions wie map/filter/reduce. Trailing-Lambda-Syntax: wenn der letzte Parameter eine Funktion ist, kann sie außerhalb () gehen. Dies ist idiomatisches Kotlin.

kotlin
val square: (Int) -> Int = { x -> x * x }
println(square(5))  // 25
val nums = listOf(1, 2, 3)
println(nums.map { it * 2 })      // [2, 4, 6]
println(nums.filter { it > 1 })   // [2, 3]
println(nums.reduce { a, b -> a + b })  // 6
// 'it' is the implicit single parameter

Erweiterungsfunktionen

Erweiterungsfunktionen fügen vorhandenen Klassen (auch aus Java) Methoden hinzu, ohne Vererbung oder Modifikation. Sie sind syntaktischer Zucker für statische Funktionen. 'this' referenziert den Empfänger. Verwenden Sie, um APIs fluent zu machen. Zur Kompilierzeit aufgelöst (kein dynamischer Dispatch).

kotlin
fun String.shout(): String = this.uppercase() + "!"
fun Int.isEven(): Boolean = this % 2 == 0
println("hello".shout())  // HELLO!
println(4.isEven())       // true
// Extensions don't modify the class—they're resolved statically
// Use to add utility methods to any type

Inline & Infix

inline kopiert den Funktions-Body zu Aufrufstellen (eliminiert Lambda-Overhead). Verwenden Sie für Higher-Order Functions in Hot Paths. infix ermöglicht Funktionsaufrufe ohne Punkt/Klammern (a op b) – verwenden Sie für DSLs und lesbare Operationen wie 'to', 'in', 'and'.

kotlin
inline fun measure(block: () -> Unit) {
    val start = System.currentTimeMillis()
    block()
    println("Took ${System.currentTimeMillis() - start}ms")
}
measure { println("working...") }
// Infix functions for readable calls
infix fun Int.times(str: String): String = str.repeat(this)
println(3 times "ab")  // ababab

Vararg & Spread

vararg akzeptiert variable Argumente (in ein Array gesammelt). * spreadet ein Array in vararg. Benannte Parameter können vararg folgen. Verwenden Sie für flexible APIs wie listOf(), printf-Style-Funktionen. Der Spread-Operator ist Kotlins Äquivalent zu JS-Spread.

kotlin
fun sum(vararg nums: Int): Int = nums.sum()
println(sum(1, 2, 3, 4))  // 10
// Spread operator to pass array
val arr = intArrayOf(1, 2, 3)
println(sum(*arr))  // 6
// Named/default params can follow vararg
fun greet(vararg names: String, greeting: String = "Hi") =
    names.joinToString { "$greeting, $it!" }
println(greet("A", "B"))
06

Klassen & OOP

Klasse & Konstruktor

Der Primary-Constructor ist im Klassen-Header. val/var-Parameter werden Properties (mit Gettern/Settern). Ohne val/var sind sie nur Konstruktor-Parameter (nicht zugreifbar). Der init-Block läuft während der Konstruktion. Kotlin generiert Getter/Setter automatisch.

kotlin
class Person(val name: String, val age: Int) {
    fun greet(): String = "Hi, I'm $name"
    fun isAdult(): Boolean = age >= 18
}
val p = Person("Alice", 30)
println(p.greet())   // Hi, I'm Alice
println(p.name)      // Alice (val = property)
println(p.isAdult()) // true
// val/var in constructor = auto property

Data Class

data class auto-generiert equals, hashCode, toString, copy und componentN (für Destrukturierung). Verwenden Sie für Datenhalter. Muss mindestens einen val/var-Parameter haben. copy() erstellt einen modifizierten Klon – toll für unveränderliche Updates. Destrukturieren mit val (a, b) = point.

kotlin
data class Point(val x: Int, val y: Int)
val p1 = Point(3, 4)
val p2 = Point(3, 4)
println(p1 == p2)        // true (value equality)
println(p1.copy(x = 5))  // Point(x=5, y=4)
println(p1)              // Point(x=3, y=4)
// Auto: equals, hashCode, toString, copy, componentN
val (x, y) = p1  // destructuring via componentN()

Sealed Class

Sealed-Klassen beschränken Subtypen auf dieselbe Datei/dasselte Kotlin-Modul. when-Ausdrücke sind erschöpfend (Compiler prüft alle Fälle). Verwenden Sie für ADTs (Algebraic Data Types), die endliche Zustände repräsentieren. Kombiniert mit when ermöglicht dies sicheres, compile-geprüftes Pattern-Matching.

kotlin
sealed class Result
data class Success(val value: Int) : Result()
data class Failure(val error: String) : Result()
object Loading : Result()
fun handle(r: Result): String = when (r) {
    is Success -> "Got ${r.value}"
    is Failure -> "Error: ${r.error}"
    Loading -> "Loading..."
}
// when is exhaustive—compiler enforces all cases

Object & Companion

object deklariert ein Singleton (eine Instanz, lazy initialisiert). companion object innerhalb einer Klasse hält 'static'-Member (via ClassName.member zugegriffen). Verwenden Sie object für Singletons, companion für Factory-Methoden und Konstanten. Companion kann Interfaces implementieren.

kotlin
object Config {  // singleton
    val version = "1.0"
    fun load() = mapOf("key" to "value")
}
println(Config.version)  // 1.0
class User(val name: String) {
    companion object {
        fun create(name: String) = User(name)
        const val MAX = 100
    }
}
val u = User.create("Alice")  // like static method

Vererbung & Interface

Klassen sind standardmäßig final – verwenden Sie 'open', um Vererbung zu erlauben. 'override' ist erforderlich. Interfaces können Standard-Implementierungen haben. Eine Klasse erweitert eine Klasse, implementiert aber mehrere Interfaces. Verwenden Sie abstract für partielle Implementierung. Bevorzugen Sie Komposition über Vererbung.

kotlin
open class Animal(val name: String) {
    open fun speak() = "..."
}
class Dog(name: String) : Animal(name) {
    override fun speak() = "Woof"
}
interface Greetable {
    val name: String
    fun greet(): String  // can have default impl
}
class Person(override val name: String) : Greetable {
    override fun greet() = "Hi, $name"
}
07

Null-Sicherheit & Scope-Funktionen

Safe-Calls & Elvis

?. Safe-Call (gibt null, wenn Empfänger null), ?: Elvis (bietet Standard). Verketten Sie ?. für tiefen Zugriff (user?.address?.city). Verwenden Sie ?: return/throw für frühes Verlassen. Diese machen Null-Behandlung prägnant und sicher – keine expliziten Null-Prüfungen nötig.

kotlin
var name: String? = "Alice"
println(name?.length)      // 5
name = null
println(name?.length)      // null
println(name?.length ?: 0) // 0 (elvis: default if null)
println(name?.length ?: return)  // early return
// Safe call chain
val city: String? = user?.address?.city

let (Null-Prüfung)

let führt einen Block nur aus, wenn der Wert non-null ist. 'it' ist der Non-Null-Wert innerhalb des Blocks. Verwenden Sie für Null-geprüfte Operationen statt if-null-Prüfungen. Häufiges Pattern: value?.let { ... } für sichere Verarbeitung. Gibt das Ergebnis des Blocks zurück.

kotlin
var name: String? = "Alice"
name?.let {
    println("Name is $it")  // only runs if not null
    println("Length: ${it.length}")  // it is non-null
}
// Common for null-guarded blocks
val result = name?.let {
    process(it)  // it: String (non-null)
} ?: "default"

apply & also

apply konfiguriert ein Objekt (gibt das Objekt zurück, 'this'-Empfänger) – ideal für Builder. also führt Nebeneffekte aus (gibt das Objekt zurück, 'it'-Parameter) – gut für Logging/Debugging in Ketten. Beide geben das Original-Objekt zurück und ermöglichen Fluent-Chains.

kotlin
val list = mutableListOf<Int>().apply {
    add(1); add(2); add(3)  // 'this' = the list
    println("Built $size items")
}
val config = Config().also {
    it.timeout = 30  // 'it' = config
    println("Configured")
}
// apply: configure and return object (this)
// also: side effects, return object (it)

run & with

run führt einen Block auf einem Objekt aus (gibt Block-Ergebnis zurück, 'this'-Empfänger) – verwenden Sie zum Transformieren eines Objekts. with ist wie run, nimmt aber das Objekt als Parameter (nicht auf Nullables verkettbar). Verwenden Sie zum Gruppieren von Operationen auf demselben Objekt.

kotlin
val result = "Hello".run {
    length  // 'this' = string, returns last expr
}  // 5
val r2 = with(StringBuilder()) {
    append("a"); append("b")
    toString()  // returns this
}
// run: object.run { } - returns block result
// with: with(obj) { } - returns block result (not chainable)

TakeIf & TakeUnless

takeIf gibt das Objekt zurück, wenn das Prädikat true ist, sonst null. takeUnless ist das Gegenteil. Verwenden Sie für bedingtes Filtern in Ketten – vermeidet separate if-Prüfungen. Kombinieren Sie mit ?: für Standardwerte. Elegant für Validierungs-Pipelines.

kotlin
val age = 25
val valid = age.takeIf { it >= 18 }  // 25 (if true)
val invalid = age.takeIf { it < 18 }  // null (if false)
val adult = age.takeUnless { it < 18 }  // 25
// Useful for filtering in chains
val email = getUser()?.email?.takeIf { it.contains("@") }
println(valid, invalid, adult)
08

Collections & Funktional

Map / Filter / Fold

map transformiert, filter wählt aus, reduce/fold aggregieren. 'it' ist das implizite Element. fold nimmt einen Seed; reduce nicht (wirft bei leer). Diese sind der Kern der funktionalen Collection-Verarbeitung – verwenden Sie statt Schleifen für Klarheit.

kotlin
val nums = listOf(1, 2, 3, 4, 5)
println(nums.map { it * 2 })          // [2,4,6,8,10]
println(nums.filter { it % 2 == 0 })  // [2,4]
println(nums.reduce { a, b -> a + b }) // 15
println(nums.fold(0) { a, b -> a + b }) // 15
println(nums.sum())                   // 15
println(nums.joinToString(", "))      // 1, 2, 3, 4, 5

FlatMap & GroupBy

flatten entfernt eine Ebene der Verschachtelung. flatMap mappt und flacht in einem Schritt ab – essenziell für verschachtelte Transformationen. groupBy partitioniert nach einem Schlüssel in eine Map. Diese sind mächtig für Datenverarbeitungs- und Analyse-Pipelines.

kotlin
val nested = listOf(listOf(1, 2), listOf(3, 4))
println(nested.flatten())         // [1,2,3,4]
println(nested.flatMap { it.map { n -> n * 2 } })  // [2,4,6,8]
val words = listOf("apple", "bat", "ant")
val byFirst = words.groupBy { it.first() }
// {a=[apple, ant], b=[bat]}
println(byFirst)

Sortierung

sorted/sortedDescending sortieren natürlich. sortedBy/sortedByDescending sortieren nach einer Schlüsselauswahl. Diese geben neue Listen zurück (unveränderlich). Für veränderliche Listen verwenden Sie sort/sortBy (In-Place). Verwenden Sie Schlüsselauswahlen zum Sortieren nach einem spezifischen Feld.

kotlin
val nums = listOf(3, 1, 4, 1, 5)
println(nums.sorted())           // [1,1,3,4,5]
println(nums.sortedDescending()) // [5,4,3,1,1]
val people = listOf("Alice" to 30, "Bob" to 25)
val byAge = people.sortedBy { it.second }  // by age
println(byAge)
val byNameDesc = people.sortedByDescending { it.first }
println(byNameDesc)

Sequences (Lazy)

Sequences sind verzögert – Operationen werden bis zu einer Terminal-Operation (toList, sum, count) aufgeschoben. Vermeidet intermediäre Collections für bessere Performance bei großen Daten. Verwenden Sie asSequence() für mehrstufige Pipelines auf großen Collections. Wie Java Streams.

kotlin
val nums = (1..1000000).toList()
// Eager: creates intermediate lists
val eager = nums.filter { it % 2 == 0 }.map { it * 2 }.take(5)
// Lazy: no intermediate collections
val lazy = nums.asSequence()
    .filter { it % 2 == 0 }
    .map { it * 2 }
    .take(5)
    .toList()  // forces evaluation
println(lazy)  // [4, 8, 12, 16, 20]

Partition & Chunked

partition teilt in zwei Listen nach einem Prädikat (gibt Pair zurück). chunked teilt in fixgrößen Listen. windowed erstellt gleitende Fenster. Diese sind nützlich für Batching, Pagination und Sliding-Window-Algorithmen. Alle geben neue Collections zurück.

kotlin
val nums = listOf(1, 2, 3, 4, 5)
val (evens, odds) = nums.partition { it % 2 == 0 }
println(evens)  // [2, 4]
println(odds)   // [1, 3, 5]
val chunked = nums.chunked(2)
println(chunked)  // [[1, 2], [3, 4], [5]]
val windowed = nums.windowed(3)
println(windowed)  // [[1,2,3], [2,3,4], [3,4,5]]
09

Coroutines & Async

Launch (Fire & Forget)

launch startet eine Coroutine, die kein Ergebnis zurückgibt (Fire-and-Forget). delay ist nicht-blockierend (im Gegensatz zu Thread.sleep). runBlocking überbrückt Sync/Async-Code (verwenden Sie in main/tests). Coroutines sind leichtgewichtig – Tausende können auf wenigen Threads laufen.

kotlin
import kotlinx.coroutines.*
fun main() = runBlocking {
    launch {
        delay(1000)
        println("World!")  // after 1s
    }
    println("Hello")  // immediately
    // launch doesn't return a result
    // Use for side-effect coroutines
}
// Output: Hello, then World! after 1s

Async & Await

async startet eine Coroutine, die ein Deferred<T> zurückgibt. await() suspendiert, bis das Ergebnis bereit ist. Starten Sie mehrere asyncs vor dem awaiten für Parallelismus. Verwenden Sie für nebenläufige Berechnungen, die Ergebnisse produzieren. Wie Promise/Future in anderen Sprachen.

kotlin
import kotlinx.coroutines.*
fun main() = runBlocking {
    val deferred = async {
        delay(1000)
        42  // return value
    }
    val result = deferred.await()
    println(result)  // 42
    // Parallel execution
    val a = async { computeA() }
    val b = async { computeB() }
    println(a.await() + b.await())  // concurrent
}

Suspend-Funktionen

suspend-Funktionen können pausieren und fortsetzen, ohne Threads zu blockieren. Sie können nur aus Coroutines oder anderen suspend-Funktionen aufgerufen werden. coroutineScope bietet einen strukturierten Scope (wartet auf alle Children). Verwenden Sie für Async-APIs – macht Async-Code synchron aussehend.

kotlin
import kotlinx.coroutines.*
suspend fun fetchUser(id: Int): String {
    delay(500)  // simulates network
    return "User $id"
}
suspend fun fetchAll(): List<String> = coroutineScope {
    val a = async { fetchUser(1) }
    val b = async { fetchUser(2) }
    listOf(a.await(), b.await())
}
// suspend functions can only be called from coroutines

Flow (Cold Stream)

Flow ist Kotlins Cold-Async-Stream (wie RxJava Observable). Werte werden bei Collection produziert. Verwenden Sie map/filter/reduce-Operatoren. emit produziert, collect konsumiert. Ideal für Streaming-Daten, Events oder pagenierte APIs. Hot-Streams verwenden SharedFlow/StateFlow.

kotlin
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
fun numbers(): Flow<Int> = flow {
    for (i in 1..5) {
        delay(100)
        emit(i)  // produce value
    }
}
fun main() = runBlocking {
    numbers().collect { println(it) }  // 1,2,3,4,5
    // Transform
    numbers().map { it * 2 }.filter { it > 4 }.collect {
        println(it)  // 6, 8, 10
    }
}

Dispatchers & Context

Dispatchers wählen den Thread-Pool: Main für UI, IO für Netzwerk/Datei (großer Pool), Default für CPU-Arbeit (Core-Anzahl). withContext wechselt den Kontext innerhalb einer Coroutine. Verwenden Sie den richtigen Dispatcher, um UI-Blockierung oder Thread-Pool-Aushungerung zu vermeiden.

kotlin
import kotlinx.coroutines.*
fun main() = runBlocking {
    launch(Dispatchers.Main) { /* UI thread */ }
    launch(Dispatchers.IO) {
        // Network/file I/O (thread pool)
        val data = fetchData()
    }
    launch(Dispatchers.Default) {
        // CPU-intensive work
        val result = heavyCompute()
    }
    // withContext switches dispatcher
    val data = withContext(Dispatchers.IO) { readFile() }
}
10

Fehlerbehandlung & I/O

Try / Catch / Finally

try/catch/finally ist wie Java, aber try ist ein Ausdruck, der einen Wert zurückgibt. Keine Checked-Exceptions – alle sind unchecked. Fangen Sie spezifische Exceptions für zielgerichtete Behandlung. Verwenden Sie finally für Cleanup. Bevorzugen Sie Result oder Nullable-Returns für erwartete Fehlschläge.

kotlin
val result = try {
    "abc".toInt()
} catch (e: NumberFormatException) {
    0  // fallback
} finally {
    println("cleanup")
}
println(result)  // 0
// try is an expression—returns a value
// Kotlin doesn't have checked exceptions

Result-Typ

Result<T> wickelt Erfolg oder Fehlschlag (wie Try in Scala). runCatching konvertiert Exceptions zu Result. getOrNull/getOrElse für sicheren Zugriff. onSuccess/onFailure für Callbacks. Verwenden Sie für erwartete Fehler statt Exceptions – sauberere funktionale Fehlerbehandlung.

kotlin
fun parse(s: String): Result<Int> =
    runCatching { s.toInt() }
val r = parse("42")
println(r.getOrNull())    // 42
println(r.getOrElse { 0 }) // 42
r.onSuccess { println("OK: $it") }
    .onFailure { println("Err: ${it.message}") }
val r2 = parse("abc")
println(r2.getOrNull())   // null

Benutzerdefinierte Exceptions

Benutzerdefinierte Exceptions erweitern Exception (oder eine Subklasse). Geben Sie eine Nachricht für Debugging. Fangen Sie spezifische Exceptions vor generischen (Reihenfolge wichtig). Verwenden Sie Exceptions für wirklich außergewöhnliche Fälle; für erwartete Fehlschläge bevorzugen Sie Result oder Nullable-Returns.

kotlin
class InvalidAgeException(message: String) : Exception(message)
fun setAge(age: Int) {
    if (age < 0) throw InvalidAgeException("Age cannot be negative: $age")
}
try {
    setAge(-5)
} catch (e: InvalidAgeException) {
    println("Caught: ${e.message}")
} catch (e: Exception) {
    println("Other: ${e.message}")
}

Datei-I/O

Kotlin verwendet Javas File mit Erweiterungsfunktionen. writeText/readText für einfache Fälle. useLines streamt Zeilen (auto-close, speichereffizient). Für große Dateien verwenden Sie bufferedReader(). Schließen Sie immer Ressourcen – verwenden Sie use { }-Block für Auto-Closing.

kotlin
import java.io.File
// Write
File("test.txt").writeText("Hello, File!")
// Read
val content = File("test.txt").readText()
println(content)  // Hello, File!
// Append
File("log.txt").appendText("new line\n")
// Line by line
File("test.txt").useLines { lines ->
    lines.forEach { println(it) }
}

JSON (kotlinx.serialization)

kotlinx.serialization ist Kotlins offizielle JSON-Bibliothek – Kompilierzeit-sicher, keine Reflection. Annotieren Sie Data-Klassen mit @Serializable. encodeToString/decodeFromString für JSON. Verwenden Sie @SerialName für benutzerdefinierte Schlüsselnamen, @Optional für Standardwerte. Alternative: Gson/Moshi.

kotlin
import kotlinx.serialization.*
import kotlinx.serialization.json.*
@Serializable
data class User(val name: String, val age: Int)
val user = User("Alice", 30)
val json = Json.encodeToString(user)
println(json)  // {"name":"Alice","age":30}
val decoded = Json.decodeFromString<User>(json)
println(decoded.name)  // Alice
// Requires @Serializable annotation
11

Coroutines Deep Dive

Strukturierte Nebenläufigkeit

Strukturierte Nebenläufigkeit bindet Child-Coroutines an einen Parent-Scope – der Parent vervollständigt nicht, bis alle Children es tun, und ein Child-Fehlschlag bricht Geschwister ab. Bevorzugen Sie coroutineScope über GlobalScope. Dies verhindert Coroutine-Lecks und macht Abbruch vorhersagbar.

kotlin
import kotlinx.coroutines.*
fun main() = runBlocking {
    // coroutineScope waits for all children
    coroutineScope {
        launch { delay(1000); println("A") }
        launch { delay(500); println("B") }
    }
    println("Done")  // after both complete
    // If a child fails, parent cancels siblings
    // Never use GlobalScope unless necessary
}

Abbruch & kooperativer Abbruch

Abbruch ist kooperativ – Coroutines müssen an Suspendierungspunkten (delay, yield, await) dafür prüfen. Abbrechbare Suspending-Funktionen werfen CancellationException. Verwenden Sie ensureActive() oder isActive zum Prüfen. Nicht-suspendierende CPU-Schleifen reagieren nicht auf Abbruch, außer Sie rufen yield() auf oder prüfen isActive.

kotlin
import kotlinx.coroutines.*
fun main() = runBlocking {
    val job = launch {
        repeat(10) { i ->
            delay(300)
            yield()  // explicit suspension point
            println("Working $i")
        }
    }
    delay(700)
    job.cancelAndJoin()  // cancels and waits
    println("Cancelled")
}
// delay/yield check for cancellation
// isActive checks cancellation state

Exception-Behandlung

Unbehandelte Exceptions in launch propagieren zum Parent (brechen Geschwister ab), außer ein CoroutineExceptionHandler ist installiert. async verschiebt Exceptions zu await(). Verwenden Sie try/catch um await. Der Handler funktioniert nur für ungefangene Exceptions in launch – verwenden Sie SupervisorJob, um Fehler zu isolieren.

kotlin
import kotlinx.coroutines.*
fun main() = runBlocking {
    val handler = CoroutineExceptionHandler { _, e ->
        println("Caught: $e")
    }
    val job = launch(handler) {
        throw RuntimeException("Boom!")
    }
    job.join()
    // async exceptions surface on await()
    val deferred = async { throw IllegalStateException() }
    try { deferred.await() } catch (e: Exception) {
        println("Async failed: $e")
    }
}

Supervision (supervisorScope)

supervisorScope (und SupervisorJob) erstellt einen Scope, in dem ein Child-Fehlschlag NICHT seine Geschwister abbricht – jedes Child scheitert unabhängig. Verwenden Sie es für unabhängige Operationen (z. B. mehrere unabhängige API-Aufrufe). Reguläres coroutineScope bricht Geschwister beim ersten Fehlschlag ab (Fail-Fast).

kotlin
import kotlinx.coroutines.*
fun main() = runBlocking {
    // supervisorScope: child failures don't cancel siblings
    supervisorScope {
        launch { delay(100); throw RuntimeException("A fails") }
        launch { delay(200); println("B still runs") }
    }
    // vs coroutineScope: A's failure cancels B
    // Use SupervisorJob for long-lived services
    val scope = CoroutineScope(SupervisorJob())
}

Channels (CSP-Style)

Channels geben Werte zwischen Coroutines weiter (wie Go-Channels). Rendezvous (Kapazität 0) synchronisiert Sender und Empfänger; gepuffert erlaubt Queuing. send suspendiert, wenn voll, receive, wenn leer. Schließen Sie immer Produzenten mit close(). Für Fan-Out verwenden Sie BroadcastChannel oder SharedFlow. Bevorzugen Sie Flow für die meisten Streaming-Bedürfnisse.

kotlin
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.*
fun main() = runBlocking {
    val channel = Channel<Int>(3)  // buffered capacity 3
    launch {
        for (i in 1..5) {
            channel.send(i)
            println("Sent $i")
        }
        channel.close()
    }
    // receive() blocks until available
    for (x in channel) println("Got $x")
    // Produce-consume pattern
    // Rendezvous (cap 0) syncs sender/receiver
}
12

Flow Deep Dive

Flow-Operatoren

Flow-Operatoren sind cold – sie führen nur aus, wenn collected. map/filter/take sind wie Stream/Sequence. transform ist am flexibelsten (kann mehrere Werte pro Eingabe emitieren). flatMap-Varianten behandeln verschachtelte Flows: concat (sequenziell), merge (nebenläufig), latest (vorherige abbrechen). Wählen Sie basierend auf Ordering/Nebenläufigkeits-Bedürfnissen.

kotlin
import kotlinx.coroutines.flow.*
fun nums() = flow {
    for (i in 1..5) emit(i)
}
suspend fun main() {
    nums().map { it * it }           // 1,4,9,16,25
        .filter { it > 5 }          // 9,16,25
        .take(2)                    // 9,16
        .collect { println(it) }
    // transform: emit multiple values
    nums().transform { x ->
        emit(x)
        emit(x * 10)
    }.collect { println(it) }
    // flatMapConcat / flatMapMerge / flatMapLatest
}

Buffer & Nebenläufigkeit

buffer entkoppelt Produzent und Konsument mit einer Queue fester Kapazität – nützlich, wenn Produktions- und Konsumptionsgeschwindigkeiten differieren. conflate behält nur den neuesten Wert (intermediäre verwerfen) für UI/Zustands-Updates. collectLatest bricht den vorherigen Collector ab, wenn ein neuer Wert ankommt – ideal für Search-as-you-type.

kotlin
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
fun events() = flow {
    for (i in 1..3) {
        delay(100); emit(i)
    }
}
fun main() = runBlocking {
    // Without buffer: producer & consumer alternate (slow)
    events().buffer()  // decouple producer/consumer
        .collect { delay(200); println(it) }
    // conflate: drop intermediate values
    events().conflate().collect { println(it) }
    // collectLatest: cancel previous collection
    events().collectLatest { delay(200); println(it) }
}

StateFlow & SharedFlow (Hot Streams)

StateFlow ist ein Hot-Stream, der einen einzelnen aktuellen Wert hält – verwenden Sie es für UI-Zustand (ersetzt LiveData). Es hat immer einen Wert und conflates. SharedFlow ist ein Hot-Broadcast-Stream (mehrere Collector, kein erforderlicher Initialwert) – verwenden Sie es für Events. SharedFlow mit replay=1 verhält sich wie ein gecachter Event-Bus.

kotlin
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
fun main() = runBlocking {
    // StateFlow: holds one value, stateful
    val state = MutableStateFlow(0)
    launch {
        state.collect { println("State: $it") }
    }
    state.value = 1  // update synchronously
    state.value = 2
    // SharedFlow: broadcasts to multiple collectors
    val events = MutableSharedFlow<String>()
    launch { events.collect { println("A: $it") } }
    launch { events.collect { println("B: $it") } }
    events.emit("Hello")  // both receive
}

Flow Exception-Behandlung

catch behandelt Upstream-Ausnahmen und kann Fallback-Werte emittieren. Es fängt nur Ausnahmen ab, die vor ihm in der Kette emittiert wurden. retry/retryWhen sammeln den Flow bei Fehlern neu (nützlich für Netzwerkaufrufe). Für Downstream-Ausnahmen den collect-Aufruf in try/catch einwickeln. Niemals innerhalb des Flow-Builders abfangen – ihn propagieren lassen.

kotlin
import kotlinx.coroutines.flow.*
fun numbers() = flow {
    emit(1)
    throw RuntimeException("fail")
    emit(2)  // never reached
}
suspend fun main() {
    // catch operator (upstream only)
    numbers().catch { e -> emit(-1) }
        .collect { println(it) }  // 1, -1
    // retry on failure
    numbers().retry(3) { e ->
        println("Retry: $e"); true
    }.collect { println(it) }
    // retryWhen for custom logic
}

flowOn & Kontext

flowOn schaltet den Dispatcher für den Upstream-Flow um (Producer + Operatoren oberhalb). Dies ist unerlässlich, wenn der Flow blockierende I/O durchführt – mit flowOn(Dispatchers.IO) umwickeln. Der Downstream (collect) läuft im Kontext des Aufrufers. Mehrere flowOn-Aufrufe erzeugen separate Kontexte für jedes Segment.

kotlin
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
fun diskFlow() = flow {
    for (i in 1..3) {
        Thread.sleep(100)  // blocking I/O
        emit(i)
    }
}
suspend fun main() {
    // flowOn changes upstream context
    diskFlow().flowOn(Dispatchers.IO)
        .collect { println(it) }
    // Without flowOn, runs on collector's dispatcher
    // flowOn applies to all operators above it
    diskFlow().map { it * 2 }
        .flowOn(Dispatchers.IO)
        .filter { it > 2 }
        .collect { println(it) }
}
13

Sealed Classes & ADTs

Sealed Classes Grundlagen

Sealed Classes beschränken Unterklassen auf eine bekannte Menge (gleiche Datei/Modul). Der Compiler kennt alle möglichen Typen und ermöglicht erschöpfende when-Ausdrücke ohne else. Ideal zur Modellierung endlicher Zustände (Result, UiState, Netzwerkantworten). Kombiniert mit Data Classes bilden sie Algebraische Datentypen.

kotlin
sealed class Result<out T> {
    data class Success<T>(val value: T) : Result<T>()
    data class Failure(val error: String) : Result<Nothing>()
    object Loading : Result<Nothing>()
}
fun handle(r: Result<Int>) = when (r) {
    is Result.Success -> println("Got ${r.value}")
    is Result.Failure -> println("Error: ${r.error}")
    Result.Loading -> println("Loading...")
}
// All subclasses defined in same file/module

Sealed Interfaces (Kotlin 1.5+)

Sealed Interfaces (Kotlin 1.5+) erweitern sealed auf Interfaces und ermöglichen einer Klasse, mehrere Sealed-Typen zu implementieren – flexibler als Sealed Classes. Die Beschränkung auf gleiche Datei/gleiches Modul gilt. Nützlich für Domain-Modellierung, wenn ein Typ zu mehreren Kategorien gehört.

kotlin
sealed interface Shape {
    fun area(): Double
}
data class Circle(val r: Double) : Shape {
    override fun area() = Math.PI * r * r
}
data class Square(val side: Double) : Shape {
    override fun area() = side * side
}
// Sealed interfaces allow multiple inheritance
sealed interface Clickable { fun click() }
class Button : Shape, Clickable {
    override fun area() = 0.0
    override fun click() = println("Clicked")
}

Erschöpfendes when

Wenn when als Ausdruck (nicht als Anweisung) verwendet wird, fordert der Compiler alle Sealed-Fälle – kein else nötig. Das Hinzufügen einer neuen Unterklasse erzeugt Kompilierfehler an nicht behandelten when-Stellen, was Refactoring sicher macht. Dies ist das Killer-Feature von Sealed Classes für Zustandsautomaten und UI-Rendering.

kotlin
sealed class NetworkState {
    object Loading : NetworkState()
    data class Success(val data: String) : NetworkState()
    data class Error(val message: String) : NetworkState()
}
fun render(state: NetworkState): String = when (state) {
    NetworkState.Loading -> "Spinner"
    is NetworkState.Success -> "Data: ${state.data}"
    is NetworkState.Error -> "Error: ${state.message}"
    // No else needed—compiler enforces all cases
}
// Adding a new subclass causes compile error
// until you handle it

ADT-Modellierung (Result/Either)

Sealed Classes modellieren Summentypen (Either/Result) für Railway-Oriented Programming. fold behandelt beide Zweige. Im Gegensatz zu Ausnahmen sind Fehler explizit in der Typsignatur. Arrow-kt bietet umfangreicheres Either/Validated. Verwenden für vorhersehbares, komponierbares Fehlerhandling ohne try/catch.

kotlin
sealed class Either<out L, out R> {
    data class Left<out L>(val value: L) : Either<L, Nothing>()
    data class Right<out R>(val value: R) : Either<Nothing, R>()
}
fun <L, R> Either<L, R>.fold(
    ifLeft: (L) -> Unit,
    ifRight: (R) -> Unit
) = when (this) {
    is Either.Left -> ifLeft(value)
    is Either.Right -> ifRight(value)
}
fun divide(a: Int, b: Int): Either<String, Int> =
    if (b == 0) Either.Left("Divide by zero")
    else Either.Right(a / b)

Sealed Classes mit Rekursion

Sealed Classes können rekursiv sein und Baumstrukturen modellieren (ASTs, JSON, Ausdrücke). Pattern Matching mit when + Rekursion wertet sie elegant aus. Dies ist klassische funktionale ADT-Verwendung – typsicher, erschöpfend und refactoring-freundlich. Verwendet in Compilern, Parsern und Konfigurations-DSLs.

kotlin
sealed class Expr {
    data class Num(val value: Int) : Expr()
    data class Add(val left: Expr, val right: Expr) : Expr()
    data class Mul(val left: Expr, val right: Expr) : Expr()
}
fun eval(e: Expr): Int = when (e) {
    is Expr.Num -> e.value
    is Expr.Add -> eval(e.left) + eval(e.right)
    is Expr.Mul -> eval(e.left) * eval(e.right)
}
val expr = Expr.Add(Expr.Num(2), Expr.Mul(Expr.Num(3), Expr.Num(4)))
fun main() = println(eval(expr))  // 14
14

Delegated Properties

lazy-Delegate

lazy verzögert die Initialisierung bis zum ersten Zugriff und speichert das Ergebnis. Standardmäßig thread-sicher (Double-Checked Locking). Verwenden für teure Ressourcen (Konfigurationen, DB-Verbindungen, Singletons), die möglicherweise nicht benötigt werden. Für Single-Threaded-Kontexte NONE-Modus übergeben, um Synchronisations-Overhead zu vermeiden.

kotlin
val heavyConfig: Config by lazy {
    println("Initializing...")
    loadConfigFromFile()  // runs once, first access
}
fun main() {
    println("Before access")
    println(heavyConfig)  // initializes here
    println(heavyConfig)  // cached, no re-init
}
// lazy is thread-safe by default (LazyThreadSafetyMode.SYNCHRONIZED)
// Use LazyThreadSafetyMode.NONE for single-threaded

observable & vetoable

observable feuert einen Callback nach jeder Änderung (Logging, Seiteneffekte). vetoable kann Änderungen durch Rückgabe von false ablehnen (Validierung). Beide nehmen einen Anfangswert und ein Lambda. Verwenden für reaktiven Zustand, Validierung oder das Auslösen von UI-Updates. Für komplexere Szenarien benutzerdefinierte Delegates verwenden.

kotlin
import kotlin.properties.Delegates
var count: Int by Delegates.observable(0) { _, old, new ->
    println("Changed: $old -> $new")
}
var age: Int by Delegates.vetoable(0) { _, old, new ->
    new >= 0  // reject negative values
}
fun main() {
    count = 1   // prints "Changed: 0 -> 1"
    count = 2   // prints "Changed: 1 -> 2"
    age = 25    // accepted
    age = -5    // rejected, stays 25
}

Benutzerdefinierter Property-Delegate

Benutzerdefinierte Delegates implementieren getValue (und setValue für vars) über ReadOnlyProperty/ReadWriteProperty. Sie kapseln wiederverwendbares Property-Verhalten (Validierung, Formatierung, Caching, DB-Mapping). thisRef ist der Eigentümer, prop sind die Property-Metadaten. Leistungsstark für ORM/Serialisierungs-Frameworks.

kotlin
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
class TrimmedString : ReadWriteProperty<Any?, String> {
    private var value: String = ""
    override fun getValue(thisRef: Any?, prop: KProperty<*>) = value
    override fun setValue(thisRef: Any?, prop: KProperty<*>, value: String) {
        this.value = value.trim()
    }
}
class User {
    var name: String by TrimmedString()
}
fun main() {
    val u = User()
    u.name = "  Alice  "
    println(u.name)  // "Alice"
}

Map-gestützte Properties

Die Delegation an eine Map ermöglicht das Binden von Properties an dynamische Schlüssel – nützlich zum Parsen von JSON, Konfigurationen oder ORM-Zeilen ohne Boilerplate. Der Property-Name muss dem Map-Schlüssel entsprechen. MutableMap für schreibbare Properties verwenden. So funktionieren kotlinx.serialization und viele ORMs unter der Haube.

kotlin
class User(map: Map<String, Any?>) {
    val name: String by map
    val age: Int by map
    val email: String? by map
}
fun main() {
    val u = User(mapOf(
        "name" to "Alice",
        "age" to 30,
        "email" to "[email protected]"
    ))
    println(u.name)  // Alice
}
// Mutable version: MutableMap
class MutableUser(map: MutableMap<String, Any?>) {
    var name: String by map
}

notNull & Singleton-Delegate

notNull() ist wie lateinit, aber für jeden Typ (einschließlich Primitives) und funktioniert mit val/var. Es wirft, wenn vor dem Setzen zugegriffen wird. Für Singletons object (eager) oder by lazy (lazy) bevorzugen. lateinit ist nur für vars in Klassen; der notNull-Delegate ist flexibler, hat aber leichten Overhead.

kotlin
import kotlin.properties.Delegates
class Service {
    // Late-init for non-null, set once
    var config: String by Delegates.notNull()
    // Throws before initialization
}
// Singleton via object (no delegate needed)
object Database {
    val connection = connect()
}
// Or lazy singleton
val db: Database by lazy { Database() }
fun main() {
    val s = Service()
    // println(s.config)  // IllegalStateException
    s.config = "prod"
    println(s.config)  // prod
}
15

DSL-Erstellung

Typsichere Builder

Typsichere Builder verwenden Funktionstypen mit Receiver (Lambda mit Receiver), um verschachtelte DSLs zu erstellen. Das Lambda läuft im Kontext des Receivers (this), sodass seine Methoden direkt aufgerufen werden. So funktionieren Kotlins HTML-DSL, Gradle-Build-Skripte und kotlinx.html – deklarativ und zur Kompilierzeit geprüft.

kotlin
class Table {
    private val rows = mutableListOf<Row>()
    fun row(init: Row.() -> Unit) {
        val r = Row(); r.init(); rows.add(r)
    }
    fun build() = rows
}
class Row {
    private val cells = mutableListOf<String>()
    fun cell(text: String) { cells.add(text) }
    fun build() = cells
}
fun table(init: Table.() -> Unit) = Table().apply(init).build()
val t = table {
    row { cell("A"); cell("B") }
    row { cell("C"); cell("D") }
}

@DslMarker (Scope-Kontrolle)

@DslMarker verhindert implizites Receiver-Leaking – innerhalb eines verschachtelten DSL-Blocks können nur Methoden des innersten Receivers aufgerufen werden. Ohne dies würde Kotlin den Aufruf äußerer Receiver-Methoden erlauben, was zu verwirrenden/fehlerhaften DSLs führt. Alle DSL-Klassen mit derselben Marker-Annotation annotieren.

kotlin
@DslMarker
annotation class HtmlDsl
@HtmlDsl
class HTML { fun body(b: Body.() -> Unit) { /*...*/ } }
@HtmlDsl
class Body { fun p(text: String) { /*...*/ } }
fun html(init: HTML.() -> Unit) = HTML().apply(init)
// Without @DslMarker, inner 'this' could call outer methods
html {
    body {
        // p() is unambiguous—Body's method
        // body() would be an error (not Body's method)
        p("Hello")
    }
}

Infix-Funktionen

infix-Funktionen ermöglichen natürlichsprachliche Syntax (a to b, 1 until 10). Sie müssen Member- oder Erweiterungsfunktionen mit einem einzelnen Parameter sein. Häufig verwendet in DSLs, Test-Frameworks und Mathe-Bibliotheken. Eingebaute Beispiele: to (Pair), until/step (Ranges) und Collections-Operationen.

kotlin
infix fun Int.times(str: String) = str.repeat(this)
fun main() {
    println(3 times "ab")  // "ababab"
    // Equivalent to: 3.times("ab")
    // No dot, no parentheses—reads like natural language
}
// Common in testing
infix fun <T> T.shouldEqual(expected: T) =
    assert(this == expected)
fun test() { 5 shouldEqual 5 }
// Also: to, until, step, in, etc.

Operator-Überladung

Operator-Überladung ermöglicht Objekten die Verwendung von +, -, *, [], () usw. mit natürlicher Syntax. Funktionen mit operator markieren. Sinnvoll überladen – Mathe-Typen (Vec, Matrix, Money) profitieren; willkürliche Überladung schadet der Lesbarkeit. get/set ermöglichen Indizierung; invoke macht Objekte aufrufbar. Range-Operatoren (rangeTo, contains) treiben for-Schleifen an.

kotlin
data class Vec(val x: Int, val y: Int) {
    operator fun plus(o: Vec) = Vec(x + o.x, y + o.y)
    operator fun minus(o: Vec) = Vec(x - o.x, y - o.y)
    operator fun times(s: Int) = Vec(x * s, y * s)
    operator fun unaryMinus() = Vec(-x, -y)
    operator fun get(i: Int) = when (i) { 0 -> x; 1 -> y; else -> throw IndexOutOfBoundsException() }
}
fun main() {
    val a = Vec(1, 2)
    val b = Vec(3, 4)
    println(a + b)        // Vec(x=4, y=6)
    println(a * 2)        // Vec(x=2, y=4)
    println(-a)           // Vec(x=-1, y=-2)
    println(a[0])         // 1
}

Funktionstypen mit Receiver

Funktionstypen mit Receiver (A.(B) -> C) ermöglichen Lambdas den Zugriff auf den Receiver als 'this'. Dies ist die Grundlage von Kotlin-DSLs und Scope-Funktionen (apply, run, with). apply gibt den Receiver zurück; run gibt das Lambda-Ergebnis zurück. Dies beherrschen, um ausdrucksstarke, typsichere interne DSLs zu bauen.

kotlin
// Lambda with receiver: A.(B) -> C
val greet: String.(Int) -> String = { times -> repeat(times) { this + "!" } }
fun main() {
    println("Hello".greet(3))  // Hello!!!Hello!!!Hello!!!
    // 'this' is the String receiver
}
// Build DSLs with receiver
class StringBuilder {
    private val parts = mutableListOf<String>()
    fun add(s: String) { parts.add(s) }
}
fun build(init: StringBuilder.() -> Unit) =
    StringBuilder().apply(init)
val sb = build { add("a"); add("b") }
16

Testing (JUnit 5, MockK, Turbine)

JUnit 5 Grundlagen

JUnit 5 (Jupiter) ist das Standard-Kotlin-Test-Framework. @Test markiert Tests; @BeforeEach/@AfterEach führen per-Test Setup/Teardown aus. Backtick-Namen für Lesbarkeit verwenden. @ParameterizedTest + @ValueSource führen einen Test mit mehreren Eingaben aus. assertThrows prüft Ausnahmen. @Disabled überspringt Tests.

kotlin
import org.junit.jupiter.api.*
import org.junit.jupiter.api.Assertions.*
class CalculatorTest {
    lateinit var calc: Calculator
    @BeforeEach fun setup() { calc = Calculator() }
    @Test fun `add two numbers`() {
        assertEquals(5, calc.add(2, 3))
    }
    @Test fun `divide by zero throws`() {
        assertThrows<ArithmeticException> { calc.divide(10, 0) }
    }
    @ParameterizedTest
    @ValueSource(ints = [1, 2, 3])
    fun `positive numbers`(n: Int) { assertTrue(n > 0) }
    @Disabled("TODO") @Test fun skip() {}
}

MockK (Mocking)

MockK ist Kotlins idiomatische Mocking-Bibliothek (behandelt final classes, Coroutines, Erweiterungsfunktionen). every { } returns/throws stubt; verify { } prüft Aufrufe. relaxed = true verwenden, um das Stubben von void-Methoden zu überspringen. MockK unterstützt suspend-Funktionen nativ über coEvery/coVerify – unerlässlich für Coroutine-Testing.

kotlin
import io.mockk.*
interface UserService { fun find(id: Int): String?; fun save(name: String): Int }
class Test {
    val mock = mockk<UserService>()
    @Test fun test() {
        every { mock.find(1) } returns "Alice"
        every { mock.find(any()) } returns null
        every { mock.save(any()) } throws RuntimeException("fail")
        verify { mock.find(1) }              // called once
        verify(exactly = 2) { mock.find(any()) }
        confirmVerified(mock)
    }
}
// relaxUnitFun / relaxed = true for void methods

Turbine (Flow-Testing)

Turbine ist die Standardbibliothek zum Testen von Flow. test { } sammelt in einem kontrollierten Scope; awaitItem() bestätigt die nächste Emission, awaitComplete()/awaitError() prüfen die Beendigung. Sie behandelt Timing und virtuelle Zeit korrekt. Mit runTest kombinieren für Coroutine-freundliche, schnelle, deterministische Flow-Tests.

kotlin
import app.cash.turbine.test
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.test.runTest
import kotlin.test.*
class FlowTest {
    @Test fun testFlow() = runTest {
        flowOf(1, 2, 3).test {
            assertEquals(1, awaitItem())
            assertEquals(2, awaitItem())
            assertEquals(3, awaitItem())
            awaitComplete()
        }
    }
    @Test fun testError() = runTest {
        flow<Int> { throw RuntimeException("x") }.test {
            assertTrue(awaitError() is RuntimeException)
        }
    }
}

Coroutines-Test (runTest)

runTest verwendet virtuelle Zeit – Delays werden sofort übersprungen, was Async-Tests schnell und deterministisch macht. StandardTestDispatcher + advanceUntilIdle() für manuelle Kontrolle verwenden. Einen TestDispatcher in Produktionscode injizieren (statt Dispatchers.Main) für Testbarkeit. runBlocking in Tests vermeiden – runTest ist der moderne Weg.

kotlin
import kotlinx.coroutines.test.*
import kotlinx.coroutines.*
import kotlin.test.*
class RepoTest {
    @Test fun fetch() = runTest {
        // Virtual time: delay(1000) completes instantly
        val result = async { delay(1000); "data" }.await()
        assertEquals("data", result)
    }
    @Test fun withDispatcher() = runTest {
        val scheduler = StandardTestDispatcher()
        val scope = CoroutineScope(scheduler)
        // advanceUntilIdle() runs pending coroutines
        scope.launch { /*...*/ }
        scheduler.advanceUntilIdle()
    }
}

Kotest-Assertionen & Styles

Kotest bietet ausdrucksstarke Assertionen (shouldBe, shouldContain) und mehrere Spec-Styles (StringSpec, FunSpec, BehaviorSpec für BDD). Es integriert sich mit Property-Based Testing und hat umfangreiche Matcher. Gegenüber JUnit für Kotlin-idiomatische, lesbare Tests wählen. Kann neben JUnit 5 laufen.

kotlin
import io.kotest.core.spec.style.StringSpec
import io.kotest.matchers.shouldBe
import io.kotest.matchers.collections.shouldContain
import io.kotest.matchers.string.shouldStartWith
class MyTest : StringSpec({
    "addition works" {
        1 + 1 shouldBe 2
    }
    "list contains" {
        listOf(1, 2, 3) shouldContain 2
    }
    "string prefix" {
        "hello" shouldStartWith "he"
    }
})
// Other styles: FunSpec, BehaviorSpec, ShouldSpec
17

Functional Programming Deep Dive

Scope-Funktionen (let, run, with, apply, also)

Scope-Funktionen unterscheiden sich durch Receiver (this vs it) und Rückgabewert (this vs Ergebnis). apply/also geben den Receiver zurück (Chaining, Builder); let/run/with geben das Lambda-Ergebnis zurück (Transformationen). apply für Konfiguration verwenden, let für Null-Prüfungen/Transformationen, also für Seiteneffekte. Nicht überbeanspruchen – Lesbarkeit zuerst.

kotlin
data class User(var name: String, var age: Int)
fun main() {
    val u = User("A", 1)
    // apply: configure & return this (builders)
    val configured = u.apply { age = 30 }
    // let: transform, it = receiver, returns lambda result
    val name = u.let { it.name.uppercase() }
    // run: transform, this = receiver, returns result
    val s = u.run { "$name:$age" }
    // with: like run but called as function
    val str = with(u) { "$name:$age" }
    // also: side effects, returns this (logging/chaining)
    u.also { println("Created $it") }
}

Sequences (Lazy Evaluation)

Sequences evaluieren verzögert – jedes Element durchläuft die gesamte Pipeline, bevor das nächste startet (wie Java Streams). Dies vermeidet Zwischen-Collections und Short-Circuits (take/find). Für große Datensätze oder mehrstufige Pipelines verwenden. Für kleine Listen ist eager List oft schneller (weniger Overhead). generateSequence baut unendliche Streams.

kotlin
fun main() {
    // Eager: List processes each step fully
    val eager = (1..10).toList()
        .map { it * 2 }
        .filter { it > 5 }
    // Lazy: Sequence processes element-by-element
    val lazy = (1..10).asSequence()
        .map { println("map $it"); it * 2 }
        .filter { println("filter $it"); it > 5 }
        .take(2)
        .toList()  // only processes until 2 found
    // Use Sequence for large/pipelined data
    // generateSequence for infinite streams
    val naturals = generateSequence(1) { it + 1 }
}

Inline-Funktionen & reified

inline eliminiert Lambda-Objekt/Allocation-Overhead durch Inlining von Bytecode – kritisch für Hot Loops. Es ermöglicht auch Non-Local Returns (Rückgabe aus der äußeren Funktion innerhalb eines Lambdas). reified-Typparameter machen generische Typinformationen zur Laufzeit verfügbar (T::class, is T), erfordern aber inline. Verwenden für typsichere Helfer wie filterIsInstance.

kotlin
// inline: copies bytecode at call site (no lambda overhead)
inline fun measure(block: () -> Unit): Long {
    val start = System.currentTimeMillis()
    block()
    return System.currentTimeMillis() - start
}
// non-local return: inline lambdas can return from outer fun
inline fun forEach(list: List<Int>, f: (Int) -> Unit) {
    for (i in list) f(i)
}
// reified: access generic type at runtime
inline fun <reified T> List<*>.filterIsInstance() =
    filter { it is T } as List<T>
val strs = listOf(1, "a", 2, "b").filterIsInstance<String>()

Higher-Order Functions

Higher-Order Functions nehmen Funktionen entgegen oder geben sie zurück. Sie sind das Rückgrat von funktionalem Kotlin – map, filter, fold, compose. Funktionstypen wie (Int) -> Int sind First-Class. compose baut Pipelines. Verwenden, um Muster zu abstrahieren, Code-Wiederverwendung zu ermöglichen und deklarativen Code zu schreiben. inline für performance-kritische Pfade markieren.

kotlin
fun <T, R> List<T>.mapTo(transform: (T) -> R): List<R> {
    val result = mutableListOf<R>()
    for (item in this) result.add(transform(item))
    return result
}
fun compose(f: (Int) -> Int, g: (Int) -> Int): (Int) -> Int =
    { x -> f(g(x)) }
fun main() {
    val double = { x: Int -> x * 2 }
    val inc = { x: Int -> x + 1 }
    val doubleThenInc = compose(inc, double)
    println(doubleThenInc(3))  // 7
    listOf(1, 2, 3).mapTo { it * it }
}

Rekursion & tailrec

tailrec konvertiert endrekursive Funktionen (bei denen Rekursion die letzte Operation ist) in Schleifen und verhindert Stack-Overflow. Der rekursive Aufruf muss in Tail-Position sein – keine ausstehende Multiplikation/Addition. Einen Akkumulator-Parameter verwenden, um Funktionen endrekursiv zu machen. Unverzichtbar für funktionale Schleifen auf großen Eingaben.

kotlin
// Regular recursion: stack overflow on large n
fun factorial(n: Int): Long =
    if (n <= 1) 1 else n * factorial(n - 1)
// tailrec: compiler optimizes to a loop (no stack growth)
tailrec fun factorialTail(n: Int, acc: Long = 1): Long =
    if (n <= 1) acc else factorialTail(n - 1, acc * n)
// Fibonacci with tailrec
tailrec fun fib(n: Int, a: Long = 0, b: Long = 1): Long =
    when (n) { 0 -> a; 1 -> b; else -> fib(n - 1, b, a + b) }
fun main() {
    println(factorialTail(10000))  // no stack overflow
}
18

Generics & Variance

Generische Klassen & Funktionen

Generics ermöglichen typsicheren, wiederverwendbaren Code. Klassen verwenden <T>; Funktionen deklarieren <T> vor dem Rückgabetyp. Constraints (T : Entity) beschränken die Typ-Schranke. Anders als Java sind Kotlins Generics für inline-Funktionen reified und haben Declaration-Site Variance, was generische APIs sicherer und ergonomischer macht.

kotlin
class Box<T>(val value: T) {
    fun get(): T = value
}
fun <T> singletonList(item: T): List<T> = listOf(item)
fun main() {
    val intBox = Box(42)        // Box<Int> inferred
    val strBox = Box<String>("hi")
    val nums = singletonList(1)
}
// Generic constraints
class Repository<T : Entity> {
    fun save(item: T) { /* T is Entity subtype */ }
}
interface Entity { val id: Int }

Variance (in/out)

Variance steuert Subtyp-Beziehungen von Generics. out (kovariant): ein Producer<Dog> ist ein Producer<Animal> – sicher, weil nur T gelesen wird. in (kontravariant): ein Sink<Animal> ist ein Sink<Dog> – sicher, weil nur T geschrieben wird. Mutable Collections sind invariant (Lesen + Schreiben). out/in verwenden, um sichere, flexible APIs zu entwerfen.

kotlin
// out (covariant): Producer<Sub> is Producer<Super>
interface Source<out T> { fun next(): T }
// in (contravariant): Consumer<Super> is Consumer<Sub>
interface Sink<in T> { fun put(item: T) }
// invariant (default): neither
class MutableList<T> {
    fun add(item: T) {}
    fun get(): T = TODO()
}
open class Animal
class Dog : Animal()
val src: Source<Animal> = Source<Dog>()  // OK (out)
val sink: Sink<Dog> = Sink<Animal>()     // OK (in)

Variance in der Praxis

Funktionstypen verwenden Variance automatisch: Parameter sind 'in', Rückgabetypen sind 'out'. Deshalb ist (Dog) -> Unit (Animal) -> Unit zuweisbar. Das PECS-Prinzip (Producer Extends, Consumer Super) entspricht Kotlins out/in. Generische Interfaces mit out entwerfen, wenn sie nur T produzieren, mit in, wenn sie nur T konsumieren.

kotlin
// Function types are variant by nature
// (T) -> R is contravariant in T, covariant in R
val dogHandler: (Dog) -> Unit = { println(it) }
val animalHandler: (Animal) -> Unit = dogHandler  // OK
// Covariant return types
interface Repository<out T> { fun find(id: Int): T }
class DogRepo : Repository<Dog> {
    override fun find(id: Int): Dog = Dog()
}
val repo: Repository<Animal> = DogRepo()  // OK
// PECS rule: Producer extends, Consumer super
// Kotlin: out = producer, in = consumer

Type Projections & Star Projection

Type Projections machen einen Typ an der Use-Site temporär variant. Array<out Any> bedeutet 'Array eines Subtyps von Any, lesbar'. Array<in Any> bedeutet 'als Any schreibbar'. Star Projection (*) bedeutet 'ein unbekannter Typ' – nützlich, wenn nur size/contains benötigt wird, nicht der Elementtyp. Wie Java-Wildcards.

kotlin
fun copy(from: Array<out Any>, to: Array<in Any>) {
    // from: only read (covariant projection)
    // to: only write (contravariant projection)
    for (i in from.indices) to[i] = from[i]
}
// Star projection: unknown type, read-only
fun printSize(list: List<*>) {
    println(list.size)  // OK (no type needed)
    // list[0]  // type is Any? — limited use
}
val mixed: List<*> = listOf(1, "a", 3.0)
// Use when type is irrelevant or unknown

Reified Type Parameters

reified bewahrt generische Typinformationen zur Laufzeit (normalerweise auf JVM gelöscht). Erfordert inline (der Typ ist an jedem Call-Site bekannt). Ermöglicht is T-Prüfungen, T::class und filterIsInstance. Ohne reified würde man einen Class<T>-Parameter manuell übergeben. Einschränkung: reified-Typen können nicht in non-inline-Funktionen oder als Klassen-Typparameter verwendet werden.

kotlin
// reified requires inline
inline fun <reified T> List<*>.filterIsInstance(): List<T> =
    filter { it is T } as List<T>
inline fun <reified T> Any.castTo(): T = this as T
inline fun <reified T> typeOf() = T::class.simpleName
fun main() {
    val list = listOf(1, "a", 2, "b", 3)
    val strs: List<String> = list.filterIsInstance()
    println(strs)  // [a, b]
    println(typeOf<String>())  // String
    // Without reified, T is erased at runtime
    // reified makes T::class and 'is T' available
}
19

Null Safety Deep

Nullable Typen

Kotlin unterscheidet nullable (T?) und non-null (T) Typen zur Kompilierzeit. ?. Safe Call gibt null zurück, wenn der Receiver null ist. ?: Elvis bietet einen Standardwert. !! wirft NPE (vermeiden). Der Compiler erzwingt Null-Prüfungen und eliminiert NullPointerException in Kotlin-Code.

kotlin
var name: String = "Alice"  // Non-null
// name = null  // Error
var nickname: String? = null  // Nullable
// Safe call
println(nickname?.length)  // null if nickname is null
// Elvis operator
val len = nickname?.length ?: 0  // 0 if null
// Not-null assertion (use sparingly)
val len2 = nickname!!.length  // NPE if null

let & run

let führt einen Block aus, wenn der Wert non-null ist (it = der Wert). run ruft einen Block mit dem Objekt als Receiver auf und gibt das Ergebnis zurück. apply konfiguriert ein Objekt und gibt es zurück. also führt Seiteneffekte aus und gibt das Objekt zurück. Diese Scope-Funktionen reduzieren Null-Prüfungen und verbessern die Lesbarkeit.

kotlin
val name: String? = "Alice"
name?.let {
    println("Length: ${it.length}")  // Only if not null
}
// run: object + block
val result = "Hello".run {
    length  // Returns 5
}
// apply: configure object
val list = mutableListOf<Int>().apply {
    add(1); add(2); add(3)
}

lateinit

lateinit verzögert die Initialisierung von non-null Properties. Kann nicht mit Primitives oder nullable Typen verwendet werden. Wirft UninitializedPropertyAccessException, wenn vor der Initialisierung zugegriffen wird. ::prop.isInitialized prüft. Nützlich für Dependency Injection und Lifecycle-verwaltete Properties.

kotlin
class Service {
    lateinit var dependency: Database
    fun init() {
        dependency = connectDatabase()
    }
    fun use() {
        if (::dependency.isInitialized) {
            dependency.query()
        }
    }
}

Nullable Collections

Collections können nullable Elemente halten (List<String?>) oder nullable sein (List<String>?). filterNotNull entfernt Nulls. firstOrNull gibt null statt zu werfen zurück. Nullable Elemente mit Safe Calls behandeln. Explizit über Nullability in Collection-Typen sein.

kotlin
val list: List<String?> = listOf("a", null, "b")
val filtered = list.filterNotNull()  // ["a", "b"]
val lengths = list.map { it?.length }  // [1, null, 1]
val first = list.firstOrNull()  // "a" or null
val firstNonEmpty = list.firstOrNull { it?.isNotEmpty() == true }

Platform Types

Platform Types entstehen aus Java-Interop, wo Nullability unbekannt ist. Kotlin kann für sie keine Null-Safety erzwingen. Für Java-Rückgabewerte immer explizit nullable Typen deklarieren. @Nullable/@NotNull-Annotationen in Java verwenden. JSR-305-Annotationen helfen Kotlin, Nullability zu inferieren.

kotlin
// Java interop: platform type (no null info)
val name: String = javaObject.getName()  // May be null!
// Kotlin does not know if Java returns null
// Fix: explicit nullable type
val name2: String? = javaObject.getName()
// Or @Nullable annotation in Java
20

DSL-Konstruktion

Builder mit Lambda Receiver

Lambda mit Receiver (T.() -> Unit) ermöglicht DSL-Syntax. Innerhalb des Lambdas ist this das Receiver-Objekt. Methoden können ohne Qualifikation aufgerufen werden. So baut Kotlin typsichere DSLs wie Gradle, HTML und SQL-Builder.

kotlin
class StringBuilder {
    private val parts = mutableListOf<String>()
    fun line(s: String) { parts.add(s) }
    fun build() = parts.joinToString("\n")
}
fun buildString(init: StringBuilder.() -> Unit): String {
    val sb = StringBuilder()
    sb.init()  // Execute lambda with sb as receiver
    return sb.build()
}

HTML-DSL

Die HTML-DSL verwendet verschachtelte Builder mit Lambda Receivern. Jedes Tag ist eine Funktion, die einen Child-Builder erstellt. Das Lambda konfiguriert das Child. Produziert typsicheres, komponierbares HTML. kotlinx.html ist eine echte Implementierung. Dasselbe Muster funktioniert für jede hierarchische Struktur.

kotlin
fun html(init: HTML.() -> Unit): HTML {
    val h = HTML(); h.init(); return h
}
class HTML {
    fun body(init: Body.() -> Unit) { /* ... */ }
}
html {
    body {
        // this: Body
        p("Hello")
    }
}

@DslMarker

@DslMarker verhindert impliziten Receiver-Zugriff auf äußere Scopes. Ohne es sind sowohl HTML- als auch Body-Methoden zugänglich, was zu Verwirrung führt. Die Annotation beschränkt den Zugriff auf den innersten Receiver. Macht DSLs typsicher und eindeutig. Unverzichtbar für komplexe DSLs.

kotlin
@DslMarker
annotation class HtmlDsl
@HtmlDsl
class HTML { fun body(...) {} }
@HtmlDsl
class Body { fun p(...) {} }
html {
    body {
        // p()  // OK: in Body scope
        // body()  // Error: in Body scope, not HTML
    }
}

Gradle DSL

Die Gradle Kotlin DSL verwendet dasselbe Builder-Muster. plugins, dependencies sind Funktionen mit Receiver-Lambdas. implementation, testImplementation sind Dependency-Konfigurationsfunktionen. Typsicher: Der Compiler prüft Funktionsnamen und Parametertypen. Viel besser als Groovy für Refactoring.

kotlin
plugins {
    kotlin("jvm") version "1.9.0"
}
dependencies {
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
    testImplementation(kotlin("test"))
}
// All are function calls with lambda receivers

Anko SQL

Anko (und Exposed) bieten typsichere SQL-DSLs. Spaltenvergleiche sind typisiert. Ein String-Spalte kann nicht mit einem Int verglichen werden. Die DSL generiert SQL. Verhindert SQL-Injection und Typfehler. Dasselbe Muster gilt für jede domänenspezifische Sprache.

kotlin
fun users(where: SqlExpressionBuilder.() -> Op<Boolean>) {
    // DSL for SQL queries
}
users {
    (Users.age greater 18) and (Users.name like "A%")
}
// Type-safe SQL: compiler checks column types
21

Collections

List-Operationen

Kotlin-Collections haben umfangreiche funktionale APIs. map, filter, reduce sind Standard. groupBy partitioniert nach Schlüssel. chunked teilt in Listen fester Größe. windowed erstellt gleitende Fenster. Alle geben neue Collections zurück. asSequence() für Lazy Evaluation verwenden.

kotlin
val list = listOf(1, 2, 3, 4, 5)
val doubled = list.map { it * 2 }
val evens = list.filter { it % 2 == 0 }
val sum = list.reduce { acc, n -> acc + n }
val grouped = list.groupBy { it % 2 }  // {0=[2,4], 1=[1,3,5]}
val chunked = list.chunked(2)  // [[1,2],[3,4],[5]]

Sequence

Sequence ist eine lazy Collection (wie Java Stream). Operationen werden bei Bedarf evaluiert. Keine Zwischen-Collections. Effizient für große Collections mit verketteten Operationen. asSequence() zum Konvertieren verwenden. Mit toList(), toSet() usw. erzwingen.

kotlin
val result = (1..1000000).asSequence()
    .map { it * 2 }
    .filter { it > 100 }
    .take(10)
    .toList()
// Lazy: only computes what is needed
// No intermediate collections

Destructuring

Destructuring zerlegt Objekte in Variablen. Funktioniert mit Pair, Triple, Data Classes und Map-Einträgen. componentN()-Funktionen ermöglichen es. withIndex() paart Index und Wert. Nützlich für mehrere Rückgabewerte und Iteration. Data Classes generieren componentN automatisch.

kotlin
val (a, b) = Pair(1, "hello")
val (x, y, z) = Triple(1, 2, 3)
for ((index, value) in list.withIndex()) {
    println("$index: $value")
}
data class Point(val x: Int, val y: Int)
val (px, py) = Point(1, 2)

Mutable vs Immutable

Kotlin hat sowohl mutable (MutableList) als auch immutable (List) Collections. Immutable für Sicherheit bevorzugen. toList() erstellt eine immutable Kopie. toMutableList() erstellt eine mutable Kopie. Die immutable Interfaces exponieren keine Mutationsmethoden und verhindern versehentliche Änderungen.

kotlin
val mutable = mutableListOf(1, 2, 3)
mutable.add(4)  // OK
val immutable = listOf(1, 2, 3)
// immutable.add(4)  // Error: no add method
// Convert
val imm = mutable.toList()
val mut = imm.toMutableList()

Associate & Partition

toMap/associate konvertieren zu Maps. partition teilt anhand eines Prädikats in zwei Listen. flatten führt verschachtelte Listen zusammen. flatMap mappt und flacht ab. Diese ersetzen umständliche Schleifen durch deklarative Ausdrücke. Alle geben neue Collections zurück.

kotlin
val list = listOf("a" to 1, "b" to 2)
val map = list.toMap()  // {a=1, b=2}
val byLength = list.associate { it.first to it.second }
val (evens, odds) = list.partition { it.second % 2 == 0 }
// evens: [(a,1)?] odds: [(b,2)]
val flat = listOf(listOf(1), listOf(2, 3)).flatten()  // [1,2,3]
22

Häufige Fallstricke

== vs ===

Kotlin == ruft equals auf (Wertgleichheit), anders als Java. === prüft Referenzgleichheit. == für Wertvergleich verwenden. === wird selten benötigt. Für Integer sind Werte -128 bis 127 gecacht, sodass === true oder false sein kann. Immer == für Werte verwenden.

kotlin
val a = Integer(127)
val b = Integer(127)
a == b  // true (value equality, calls equals)
a === b  // true (Integer caches -128 to 127)
val c = Integer(128)
val d = Integer(128)
c == d  // true
c === d  // false (not cached)

Companion Object

Kotlin hat keine statischen Member. companion object hält "statische" Methoden und Konstanten. const val ist eine echte Kompilierzeit-Konstante. Das companion object ist eine Singleton-Instanz. @JvmStatic macht Methoden als statisch aus Java aufrufbar. Top-Level-Funktionen für echte Statics verwenden.

kotlin
class MyClass {
    companion object {
        const val CONSTANT = 42
        fun create() = MyClass()
    }
}
MyClass.CONSTANT  // 42
MyClass.create()  // Static-like access
// companion object is a real object, not static

Data Class Copy

Data Classes generieren automatisch copy(), das eine modifizierte Kopie erstellt. Das Original bleibt unverändert (immutable). Nur angegebene Felder ändern sich. Nützlich für Updates. Kombiniert mit Destructuring sind Data Classes mächtig zur Datenmodellierung. var in Data Classes für Immutability vermeiden.

kotlin
data class User(val name: String, val age: Int)
val alice = User("Alice", 30)
val older = alice.copy(age = 31)  // New instance
// alice is unchanged
// copy uses named arguments for changed fields

Sealed Class Exhaustiveness

Sealed Classes beschränken Subtypen auf dieselbe Datei/dasselbe Package. when auf Sealed Classes ist erschöpfend: Der Compiler warnt, wenn ein Zweig fehlt. Kein else nötig, wenn alle Fälle abgedeckt sind. Das Hinzufügen eines neuen Subtyps verursacht Warnungen überall. Ideal für Zustandsautomaten und Results.

kotlin
sealed class Result
data class Success(val value: Int) : Result()
data class Failure(val error: String) : Result()
fun handle(r: Result) = when (r) {
    is Success -> r.value
    // Warning: Missing Failure branch
}
// Fix: handle all cases
fun handle(r: Result) = when (r) {
    is Success -> r.value
    is Failure -> 0
}  // No else needed

Extension vs Member

Member-Funktionen haben immer Vorrang vor Erweiterungsfunktionen mit derselben Signatur. Erweiterungen werden zur Kompilierzeit aufgelöst (statischer Dispatch), Member zur Laufzeit (dynamischer Dispatch). Erweiterungen können Member nicht überschreiben. Erweiterungen für Utility-Funktionen verwenden, nicht für Polymorphismus.

kotlin
class Foo {
    fun bar() = "member"  // Member function
}
fun Foo.bar() = "extension"  // Extension function
Foo().bar()  // "member" - members win!
// Extensions are resolved statically
// Members are resolved dynamically (virtual)
23

Kotlin Multiplatform

Common Code

Kotlin Multiplatform (KMP) teilt Code plattformübergreifend. expect/actual deklariert plattformspezifische Implementierungen. commonMain enthält geteilten Code. Plattformspezifische Source-Sets implementieren actual. Business-Logik teilen, UI nativ halten. Gradle konfiguriert Targets.

kotlin
// commonMain/src/Platform.kt
expect fun getPlatformName(): String
// androidMain/src/Platform.kt
actual fun getPlatformName(): String = "Android"
// iosMain/src/Platform.kt
actual fun getPlatformName(): String = "iOS"
// Shared business logic
class Greeting {
    fun greet() = "Hello from ${getPlatformName()}"
}

Shared Module

Multiplatform-Projekte verwenden den kotlin-Block in Gradle. Targets definieren (android, ios). commonMain hat geteilte Dependencies. Plattform-Source-Sets können plattformspezifische Deps haben. iOS verwendet Kotlin/Native für direkte Kompilierung. Logik teilen, nicht UI.

kotlin
// build.gradle.kts (shared module)
kotlin {
    androidTarget()
    iosX64(); iosArm64(); iosSimulatorArm64()
    sourceSets {
        val commonMain by getting {
            dependencies { implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3") }
        }
    }
}

Networking (Ktor)

Ktor ist ein Multiplatform-HTTP-Client. Die API ist gemeinsam, die Engine ist plattformspezifisch. Android verwendet OkHttp/Android-Engine. iOS verwendet Darwin. Coroutines funktionieren plattformübergreifend. Networking, Datenbank (SQLDelight) und Business-Logik teilen.

kotlin
// Shared HTTP client
suspend fun fetchUser(): User {
    return client.get("https://api.example.com/user").body()
}
// Platform-specific engine
// Android: OkHttp or Android
// iOS: Darwin
// Common code uses the same API

SQLDelight

SQLDelight generiert typsicheres Kotlin aus SQL. .sq-Dateien enthalten SQL mit benannten Queries. Generiert typsichere Query-Objekte. Funktioniert plattformübergreifend (Android, iOS, JVM). SQL ist die Source of Truth. Schema-Migrationen werden verfolgt. Alternative zu Room für Multiplatform.

kotlin
// .sq file: User.sq
CREATE TABLE User (id INTEGER, name TEXT);
selectById: SELECT * FROM User WHERE id = ?;
insertUser: INSERT INTO User (id, name) VALUES (?, ?);
// Generated Kotlin code
val queries: UserQueries = database.userQueries
queries.insertUser(1, "Alice")
val user = queries.selectById(1).executeAsOne()

Compose Multiplatform

Compose Multiplatform erweitert Jetpack Compose auf iOS, Desktop und Web. UI-Code plattformübergreifend teilen. Dieselbe @Composable-API. Plattformspezifische Einstiegspunkte. Für iOS noch experimentell. Reduziert UI-Duplikation. Gradle-Plugin: compose-multiplatform.

kotlin
// Shared UI with Compose Multiplatform
@Composable
fun Greeting(name: String) {
    Text("Hello, $name!")
}
// Android
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent { Greeting("World") }
    }
}
// Desktop
fun main() = application {
    Window { Greeting("Desktop") }
}
24

Testing Kotlin

JUnit 5

JUnit 5 ist das Standard-Test-Framework. Kotlin erlaubt Backtick-Testnamen für Lesbarkeit. assertEquals, assertThrows sind häufige Assertionen. @BeforeEach, @AfterEach für Setup/Teardown. @ParameterizedTest für datengetriebene Tests. kotlin.test für Multiplatform verwenden.

kotlin
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.Assertions.*
class CalculatorTest {
    @Test
    fun `test addition`() {
        val calc = Calculator()
        assertEquals(5, calc.add(2, 3))
    }
    @Test
    fun `throws on negative`() {
        assertThrows<IllegalArgumentException> { calc.sqrt(-1) }
    }
}

MockK

MockK ist eine Kotlin-native Mocking-Bibliothek. Unterstützt final classes und Erweiterungsfunktionen. every stubt, verify prüft Aufrufe. @MockK erstellt Mocks, @InjectMockKs injiziert sie. coEvery/coVerify für Coroutines. Bessere Kotlin-Unterstützung als Mockito.

kotlin
class UserServiceTest {
    @MockK
    lateinit var repo: UserRepository
    @InjectMockKs
    lateinit var service: UserService
    @BeforeEach
    fun setup() {
        MockKAnnotations.init(this)
        every { repo.find(1) } returns User("Alice")
    }
    @Test
    fun `test find`() {
        assertEquals("Alice", service.find(1).name)
        verify { repo.find(1) }
    }
}

Kotest

Kotest ist ein Kotlin-first Test-Framework. Mehrere Styles: StringSpec, BehaviorSpec, FunSpec. shouldBe ist eine fließende Assertion. Property-Testing mit Arb. Unterstützt datengetriebene Tests. Integriert mit Spring und Ktor. Kotlin-idiomatischer als JUnit.

kotlin
import io.kotest.core.spec.style.StringSpec
import io.kotest.matchers.shouldBe
class CalculatorTest : StringSpec({
    "addition should work" {
        Calculator().add(2, 3) shouldBe 5
    }
    "should throw on negative" {
        shouldThrow<IllegalArgumentException> { Calculator().sqrt(-1) }
    }
})

Coroutine-Testing

runTest aus kotlinx-coroutines-test bietet virtuelle Zeit. Delays werden sofort übersprungen. advanceUntilIdle führt ausstehende Coroutines aus. Viel schneller als Echtzeit. TestDispatcher für feine Kontrolle verwenden. Die Turbine-Bibliothek testet Flow-Emissionen.

kotlin
@Test
fun `test async`() = runTest {
    val result = fetchData()
    assertEquals("data", result)
}
// runTest replaces runBlocking with virtual time
// advanceUntilIdle() runs all pending coroutines
// Delay skips instantly

Turbine (Flow-Testing)

Turbine testet Flow-Emissionen. awaitItem holt die nächste Emission. awaitComplete prüft Beendigung. awaitError prüft einen Fehler. test-Block suspendiert, bis der Flow abgeschlossen ist. Viel sauberer als manuelles Sammeln. Unverzichtbar für Flow-Testing.

kotlin
@Test
fun `test flow`() = runTest {
    flowOf(1, 2, 3).test {
        awaitItem() shouldBe 1
        awaitItem() shouldBe 2
        awaitItem() shouldBe 3
        awaitComplete()
    }
}
// Test errors
flow<Int> { throw Exception() }.test {
    awaitError() shouldBe Exception()
}
25

Coroutines Deep Dive

Coroutine Scope

CoroutineScope definiert eine Lebensdauer für Coroutines. viewModelScope wird bei VM-Clear automatisch abgebrochen. SupervisorJob verhindert, dass Child-Fehler Geschwister abbrechen. Benutzerdefinierte Scopes benötigen expliziten Abbruch. Scopes propagieren Abbruch an Children. GlobalScope in Produktion nie verwenden (kann nicht abgebrochen werden).

kotlin
class MyViewModel : ViewModel() {
    fun fetchData() {
        viewModelScope.launch {
            val data = api.getData()
            _data.value = data
        }
    }
}
// Custom scope
val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
scope.launch { /* ... */ }
scope.cancel()  // Cancel all children

Dispatchers

Dispatchers leiten Coroutines an Thread-Pools weiter. Main: UI-Thread (Android). IO: blockierende I/O (64+ Threads). Default: CPU-Arbeit (CPU-Anzahl Threads). Unconfined: Aufrufer-Thread (fortgeschritten). withContext schaltet den Dispatcher temporär um. Die Wahl des richtigen Dispatchers verbessert Performance und verhindert UI-Freezes.

kotlin
launch(Dispatchers.Main) { updateUI() }
launch(Dispatchers.IO) { writeFile() }
launch(Dispatchers.Default) { heavyComputation() }
launch(Dispatchers.Unconfined) { runAnywhere() }
// Switch context
withContext(Dispatchers.IO) {
    val data = readDisk()
}

Flow-Operatoren

Flow-Operatoren: map transformiert, filter selektiert, flatMapMerge/flatMapConcat verkettet Flows. Cold Flows starten pro Collector neu. StateFlow hält einen Wert (wie LiveData). SharedFlow broadcastet an mehrere Collector. stateIn konvertiert Cold zu Hot. buffer/conflate steuern Backpressure.

kotlin
flowOf(1, 2, 3, 4, 5)
    .map { it * 2 }
    .filter { it > 4 }
    .collect { println(it) }  // 6, 8, 10

// Cold flow: emits per collector
// StateFlow: hot, stateful
// SharedFlow: hot, broadcast
val state = MutableStateFlow(0)
state.value = 1

Exception Handling

CoroutineExceptionHandler fängt nicht abgefangene Ausnahmen in launch ab. async-Ausnahmen propagieren zu await. SupervisorJob isoliert Child-Fehler. CancellationException ist besonders: erneut geworfen, nicht von catch abgefangen. Niemals CancellationException schlucken. try/finally oder use() für Cleanup verwenden. Abbruch propagiert durch suspend-Aufrufe.

kotlin
val handler = CoroutineExceptionHandler { _, e ->
    Log.e("TAG", "Caught: $e", e)
}
scope.launch(handler) {
    throw RuntimeException("oops")
}
// try/catch in coroutine
try {
    riskyCall()
} catch (e: Exception) {
    // handle
}

Channels

Channels ermöglichen Coroutine-zu-Coroutine-Kommunikation. send suspendiert, wenn voll, receive suspendiert, wenn leer. capacity: buffered (RENDEZVOUS=0, UNLIMITED, CONFLATED). close() signalisiert Beendigung. produce erstellt eine Producer-Coroutine. Channel ist hot: Werte werden einmal konsumiert. Für die meisten Anwendungsfälle Flow bevorzugen.

kotlin
val channel = Channel<Int>(capacity = 10)
launch {
    for (i in 1..5) channel.send(i)
    channel.close()
}
launch {
    for (x in channel) println(x)
}
// Produce pattern
fun numbers() = produce {
    for (i in 1..10) send(i)
}

Was this helpful?

Learning path

Learn from scratch

Learn this language from the ground up with structured lessons.