thecodex.expert · The Codex Family of Knowledge
Kotlin

Idioms and Stdlib

One line of Kotlin doing what took fifty in Java — data classes, extension functions, and scope functions are Kotlin's conciseness multipliers.

Kotlin 2.x 40% less code than Java Last verified:
Canonical Definition

Kotlin's idioms — data classes (auto-generated equals, hashCode, toString, copy), extension functions (add methods to existing types without inheritance), scope functions (let, run, apply, also, with) — and its rich collections API dramatically reduce boilerplate compared to Java while remaining 100% interoperable.

The central idea

Java's data classes require: fields, constructor, getters, setters, equals(), hashCode(), toString() — easily 50+ lines. Kotlin's data class User(val name: String, val age: Int) generates all of that in one line. Extension functions let you add String.isValidEmail() without touching the String class. These are the two features that make Kotlin feel like a productivity multiplier.

Data classes

Kotlindata_classes.kt
// One line replaces ~50 lines of Java
data class User(val name: String, val age: Int, val email: String)

fun main() {
    val alice = User("Alice", 30, "alice@example.com")
    val bob   = User("Bob",   25, "bob@example.com")

    // toString() auto-generated
    println(alice)   // User(name=Alice, age=30, email=alice@example.com)

    // equals() compares by content, not reference
    val alice2 = User("Alice", 30, "alice@example.com")
    println(alice == alice2)   // true (structural equality)

    // copy(): create a modified version without mutation
    val olderAlice = alice.copy(age = 31)
    println(olderAlice)   // User(name=Alice, age=31, email=alice@example.com)
    println(alice)        // unchanged

    // Destructuring declarations — component functions auto-generated
    val (name, age, email) = alice
    println("$name is $age years old")

    // Works in for loops
    val users = listOf(alice, bob)
    for ((n, a) in users) {
        println("$n: $a")
    }
}

Extension functions

Extension functions add new methods to existing types without modifying the class or using inheritance. They are resolved statically at compile time — not virtual dispatch.

Kotlinextensions.kt
// Extend String with a new method
fun String.isValidEmail(): Boolean {
    return contains("@") && contains(".")
}

// Extend Int with a range check
fun Int.isInRange(min: Int, max: Int) = this in min..max

// Extension on nullable type
fun String?.orEmpty(): String = this ?: ""

fun main() {
    println("alice@example.com".isValidEmail())   // true
    println("not-an-email".isValidEmail())        // false
    println(42.isInRange(1, 100))                 // true

    val s: String? = null
    println(s.orEmpty())   // "" (extension on nullable)

    // Stdlib extensions you use constantly:
    val nums = listOf(1, 2, 3, 4, 5)
    println(nums.filter { it > 2 })          // [3, 4, 5]
    println(nums.map { it * it })            // [1, 4, 9, 16, 25]
    println(nums.sumOf { it })               // 15
    println(nums.groupBy { it % 2 == 0 })   // {false=[1,3,5], true=[2,4]}
}

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

Scope functions execute a block of code in the context of an object. The five functions differ in two ways: how they refer to the object (it or this) and what they return (the object itself or the lambda result).

Kotlinscope_fns.kt
data class Config(var host: String = "", var port: Int = 0, var debug: Boolean = false)

fun main() {
    // apply: configure an object — returns the object; "this" inside
    val config = Config().apply {
        host = "localhost"   // this.host = ...
        port = 8080
        debug = true
    }
    println(config)   // Config(host=localhost, port=8080, debug=true)

    // let: transform a value — returns lambda result; "it" inside
    // Most useful for null-safe operations
    val name: String? = "Alice"
    val greeting = name?.let { "Hello, $it!" } ?: "Hello!"
    println(greeting)   // Hello, Alice!

    // also: side effect — returns the object; "it" inside
    val nums = mutableListOf(1, 2, 3)
        .also { println("Before: $it") }
        .apply { add(4) }
        .also { println("After: $it") }

    // run: compute a result — returns lambda result; "this" inside
    val length = "Hello Kotlin".run {
        println("String: $this")
        length   // lambda result
    }
    println(length)   // 12

    // with: non-extension version of run — pass object as argument
    val result = with(config) {
        "$host:$port (debug=$debug)"
    }
    println(result)   // localhost:8080 (debug=true)
}

Collections: immutable by default

Kotlin distinguishes read-only collections (List, Map, Set) from mutable collections (MutableList, MutableMap, MutableSet). listOf() returns a read-only view — it doesn't expose add() or remove(). mutableListOf() returns a MutableList. The read-only interface is a compile-time contract — at runtime, listOf() may return a Java ArrayList, but since the reference is typed as List<T>, the mutable methods are inaccessible.

Kotlincollections.kt
fun main() {
    val immutable = listOf(3, 1, 4, 1, 5, 9)
    // immutable.add(2)   // COMPILE ERROR: unresolved reference: add

    val mutable = mutableListOf(3, 1, 4, 1, 5, 9)
    mutable.add(2)

    // Rich collections API
    val words = listOf("banana", "apple", "cherry", "apricot", "blueberry")

    // filter, map, sorted, grouped
    val aWords = words.filter { it.startsWith("a") }.sorted()
    println(aWords)   // [apple, apricot]

    val byLength = words.groupBy { it.length }
    println(byLength)   // {6=[banana, cherry], 5=[apple], 7=[apricot], 9=[blueberry]}

    // flatMap: flatten a list of lists
    val sentences = listOf("hello world", "foo bar baz")
    val allWords = sentences.flatMap { it.split(" ") }
    println(allWords)   // [hello, world, foo, bar, baz]

    // associate: build a map from a collection
    val wordLengths = words.associateWith { it.length }
    println(wordLengths)   // {banana=6, apple=5, ...}

    // partition: split into two lists
    val (short, long) = words.partition { it.length <= 5 }
    println("Short: $short, Long: $long")
}
Commonly confused
Extension functions are resolved statically, not dynamically. If you have open class Animal and class Dog : Animal, and define an extension fun Animal.speak(), calling val d: Animal = Dog(); d.speak() calls Animal.speak() even though the runtime type is Dog. Extensions are not virtual — they cannot be overridden. If you need polymorphic behaviour, define a member function (or use an interface) instead.
apply returns the receiver; let returns the lambda result. The most common mix-up with scope functions. Use apply when configuring an object and want the object back. Use let when transforming a value (especially null-safe). A quick mnemonic: apply → configure and return self; let → transform and return new value.
listOf() and mutableListOf() are different types at compile time but may share the same backing class at runtime. Never cast a List<T> to MutableList<T> — even if it works at runtime, it violates the contract and can cause unexpected mutations in shared collections. Use toMutableList() to create a mutable copy.

Extension functions: dispatch and the inline keyword

Extension functions are syntactic sugar — at the JVM level, fun String.isValidEmail() compiles to a static method StringExtensionsKt.isValidEmail(String receiver). The compiler rewrites call sites to call this static method. Because they are static, extension functions cannot override member functions — if a class defines fun toString() and an extension fun Any.toString() exists, the member function always wins. The inline keyword on a function (often a lambda-accepting function) instructs the compiler to copy the function body and the lambda body to the call site — eliminating the overhead of lambda object creation and virtual dispatch. This is how filter, map, and the scope functions are implemented — they are inline, making collections pipelines have near-zero overhead compared to explicit loops.

Kotlin Multiplatform and expect/actual

Kotlin Multiplatform (KMP) allows sharing Kotlin code between JVM, Android, iOS (Kotlin/Native), JavaScript, and WebAssembly targets. The expect/actual mechanism is the bridge: expect fun currentTimeMs(): Long in shared code declares an API; each platform provides the actual fun currentTimeMs(): Long with the platform-specific implementation. Data classes, sealed classes, and the collections API work identically across all targets. Coroutines have multiplatform support — the same coroutine code runs on Android and iOS. KMP is increasingly used for sharing business logic between mobile platforms while keeping platform-specific UI code in Swift/SwiftUI (iOS) and Jetpack Compose (Android).

Specification reference

Kotlin documentation. kotlinlang.org/docs/. Sections: idioms, collections, extensions, scope functions. Jemerov, D. & Isakova, S. (2017). Kotlin in Action. Manning Publications. Chapters 3 (functions and lambdas), 5 (lambdas and collections), 11 (DSLs). Kotlin standard library API reference: kotlinlang.org/api/latest/jvm/stdlib/.

Sources

1
Kotlin documentation. Idioms. kotlinlang.org/docs/idioms.html.
2
Kotlin documentation. Collections overview. kotlinlang.org/docs/collections-overview.html.
3
Kotlin documentation. Scope functions. kotlinlang.org/docs/scope-functions.html.
4
Jemerov, D. & Isakova, S. (2017). Kotlin in Action. Manning. — Chapters 3, 5, 11.
5
Kotlin standard library API. kotlinlang.org/api/latest/jvm/stdlib/. — Full stdlib reference.
Source confidence: High Last verified: Primary source: Kotlin documentation — kotlinlang.org/docs