Skip to content

Kotlin Шпаргалка

Современный JVM-язык, лаконичный и полностью Java-совместимый.

01

Основы

Переменные и типы

Предпочитайте val (неизменяемое) var (изменяемое). Kotlin выводит типы, но допускает явные аннотации. Используйте 'is' для проверок типа (smart-cast автоматически). Всё — объект, нет примитивов в синтаксисе Kotlin.

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

Строковые шаблоны

$var для простых переменных, ${expr} для выражений. Строки в тройных кавычках сохраняют переносы — используйте trimIndent() для чистого форматирования. Строковые шаблоны делают построение строк лаконичным и читаемым без строк форматирования.

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-безопасность

Null-безопасность Kotlin: ? помечает типы, допускающие null, ?. безопасный вызов (возвращает null если null), !! утверждает не-null (вызывает NPE если null), ?: elvis (по умолчанию если null). Это устраняет NullPointerException во время компиляции — ключевая функция Kotlin.

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

Преобразование типов

Kotlin требует явного преобразования типов (нет неявного расширения, как в Java). Используйте toInt()/toDouble()/toString() для преобразования. toIntOrNull() возвращает null при неудаче — безопаснее toInt(), который вызывает исключение. Всегда обрабатывайте null из разбора.

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)

Проверки типа и smart-cast

is проверяет тип и smart-cast переменную в этой ветке — без явного приведения. !is — отрицание. Используйте as для небезопасного приведения (вызывает ClassCastException), as? для безопасного (возвращает null при неудаче). Smart cast — крупное удобство Kotlin.

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

Строки

Распространённые строковые методы

Строки Kotlin имеют богатые функции расширения из стандартной библиотеки. Большинство возвращают новые строки (неизменяемые). Используйте их вместо ручных циклов. Функции вроде uppercase() учитывают локаль — используйте uppercase(Locale.ROOT) для согласованности.

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

Многострочные и сырые строки

Строки в тройных кавычках сырые — не нужны escape-последовательности (кроме тройных двойных кавычек). trimMargin('|') сохраняет только текст после | для чистого отступа. trimIndent() удаляет общий пробел. Идеально для SQL, JSON, HTML в коде.

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

Построение строк

joinToString — идиоматичный способ соединения коллекций с разделителями, префиксом, суффиксом. buildString предоставляет область StringBuilder для лаконичного построения. Избегайте повторного + в циклах — используйте joinToString или StringBuilder для эффективности.

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(";") }
}

Строка в число

toInt()/toDouble() вызывают NumberFormatException при некорректном вводе. toIntOrNull() возвращает null — парируйте с ?: для безопасных значений по умолчанию. toBooleanStrict() принимает только 'true'/'false'. Используйте OrNull-варианты для пользовательского ввода или ненадёжных данных.

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 оборачивает Pattern Java. find возвращает MatchResult? (первое совпадение), findAll возвращает все. matches проверяет полное совпадение, containsMatchIn — частичное. Используйте строки в тройных кавычках для шаблонов во избежание двойного экранирования обратных слэшей.

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

Структуры данных

Список

listOf создаёт неизменяемые списки, mutableListOf для изменяемых. Списки индексируются с нуля. Используйте first()/last() для концов (вызывают исключение при пустом), firstOrNull() для безопасности. contains проверяет принадлежность. Предпочитайте неизменяемые списки, если не нужно изменять.

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 создаёт неизменяемые map, mutableMapOf для изменяемых. Инфикс 'to' создаёт пары. Доступ [] возвращает nullable V? (null если отсутствует). Используйте getOrDefault или getValue (вызывает исключение, если отсутствует). Итерируйте с деструктуризацией (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

Множество

setOf создаёт неизменяемые множества, mutableSetOf для изменяемых. union/intersect/subtract возвращают новые множества. Множества обеспечивают уникальность — add возвращает false, если элемент существует. Используйте для дедупликации и операций над множествами. LinkedHashSet сохраняет порядок вставки.

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

Массив и примитивные массивы

arrayOf создаёт Array<T> (упакованный для примитивов). Используйте intArrayOf/doubleArrayOf для неупакованных примитивных массивов (лучшая производительность). Array(size) { init } создаёт с функцией инициализации. Массивы изменяемы; предпочитайте List для большинства случаев.

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 и Triple группируют 2-3 значения. Инфикс 'to' создаёт Pair (часто для map). Деструктурируйте через val (a, b) = pair. Для более 3 значений или именованных полей используйте data-классы — они яснее и поддерживаемее.

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

Поток управления

If / Else (выражение)

В Kotlin if/else — выражение, возвращающее значение. Это заменяет тернарный оператор. Обе ветви должны возвращать совместимые типы. Используйте для лаконичного условного присваивания. Как оператор (без else), возвращает Unit.

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 — мощный switch Kotlin — поддерживает литералы, диапазоны (in), отрицание (!in), несколько значений (запятая) и is-проверки. Должно быть исчерпывающим при использовании как выражение с sealed-типами. Без субъекта — более чистая if/else цепочка.

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 и диапазоны

.. — включающий диапазон, until — исключающий, downTo считает вниз, step задаёт шаг. withIndex() даёт индексированную итерацию. for-in работает с любым Iterable. Эти диапазоны лаконичны и читаемы для счётных циклов.

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 проверяет до, do-while проверяет после (выполняется хотя бы один раз). Оба требуют изменяемого состояния. Предпочитайте циклы for или функциональные операции (map, filter) для неизменяемости. Используйте while для циклов, управляемых условием, где счётчик неизвестен.

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 (метки)

continue пропускает к следующей итерации, break выходит из цикла. Метки (@outer) обеспечивают выход из вложенных циклов — определите через label@ перед циклом, break/continue с @label. Используйте умеренно; рефакторинг в функции часто чище.

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

Функции и лямбды

Определение функции

Функции используют fun name(params): ReturnType. Тело-выражение (= expr) для одиночных выражений. Поддерживаются параметры по умолчанию и именованные аргументы. Возврат Unit = только побочный эффект. Параметры по умолчанию уменьшают необходимость перегрузки.

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

Лямбды и функции высшего порядка

Лямбды: { params -> body }. 'it' — сокращение для одного параметра. Передавайте лямбды в функции высшего порядка вроде map/filter/reduce. Синтаксис завершающей лямбды: если последний параметр — функция, она может идти за (). Это идиоматичный 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

Функции расширения

Функции расширения добавляют методы к существующим классам (даже из Java) без наследования или модификации. Это синтаксический сахар для статических функций. 'this' ссылается на получателя. Используйте для fluent API. Разрешаются во время компиляции (без динамической диспетчеризации).

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 копирует тело функции в места вызова (устраняет накладные расходы лямбды). Используйте для функций высшего порядка в горячих путях. infix включает вызовы функций без точки/скобок (a op b) — используйте для DSL и читаемых операций вроде '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 принимает переменные аргументы (собираются в массив). * разворачивает массив в vararg. Именованные параметры могут следовать за vararg. Используйте для гибких API вроде listOf(), printf-подобных функций. Оператор spread — эквивалент spread JS в Kotlin.

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

Классы и ООП

Класс и конструктор

Первичный конструктор в заголовке класса. Параметры val/var становятся свойствами (с getter/setter). Без val/var они просто параметры конструктора (недоступны). Блок init выполняется при конструировании. Kotlin генерирует getter/setter автоматически.

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-класс

data class автогенерирует equals, hashCode, toString, copy и componentN (для деструктуризации). Используйте для держателей данных. Должен иметь хотя бы один val/var параметр. copy() создаёт модифицированный клон — отлично для неизменяемых обновлений. Деструктурируйте через 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-класс

Sealed-классы ограничивают подтипы тем же файлом/модулем kotlin. Выражения when исчерпывающие (компилятор проверяет все случаи). Используйте для ADT (алгебраических типов данных), представляющих конечные состояния. Сочетая с when, обеспечивает безопасное, проверяемое на этапе компиляции сопоставление шаблонов.

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 объявляет singleton (один экземпляр, лениво инициализируется). companion object внутри класса содержит 'static' члены (доступ через ClassName.member). Используйте object для синглтонов, companion для фабричных методов и констант. Companion может реализовывать интерфейсы.

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

Наследование и интерфейс

Классы по умолчанию final — используйте 'open' для разрешения наследования. 'override' обязателен. Интерфейсы могут иметь реализации по умолчанию. Класс расширяет один класс, но реализует несколько интерфейсов. Используйте abstract для частичной реализации. Предпочитайте композицию наследованию.

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-безопасность и scope-функции

Безопасные вызовы и Elvis

?. безопасный вызов (возвращает null, если получатель null), ?: elvis (предоставляет значение по умолчанию). Цепочка ?. для глубокого доступа (user?.address?.city). Используйте ?: return/throw для ранних выходов. Это делает обработку null лаконичной и безопасной — без явных проверок null.

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)

let выполняет блок только если значение не null. 'it' — не-null значение внутри блока. Используйте для null-защищённых операций вместо if-null проверок. Распространённый паттерн: value?.let { ... } для безопасной обработки. Возвращает результат блока.

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 настраивает объект (возвращает объект, 'this' получатель) — идеально для построителей. also выполняет побочные эффекты (возвращает объект, 'it' параметр) — хорошо для логирования/отладки в цепочках. Оба возвращают исходный объект, обеспечивая fluent-цепочки.

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 выполняет блок на объекте (возвращает результат блока, 'this' получатель) — используйте для преобразования объекта. with похож на run, но принимает объект как параметр (не цепляется на nullable). Используйте для группировки операций над одним объектом.

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 возвращает объект, если предикат истинен, иначе null. takeUnless — наоборот. Используйте для условной фильтрации в цепочках — избегает отдельных if-проверок. Комбинируйте с ?: для значений по умолчанию. Элегантно для конвейеров валидации.

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

Коллекции и функциональное

Map / Filter / Fold

map преобразует, filter выбирает, reduce/fold агрегирует. 'it' — неявный элемент. fold принимает seed; reduce не делает (вызывает исключение при пустом). Это ядро функциональной обработки коллекций — используйте вместо циклов для ясности.

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 удаляет один уровень вложенности. flatMap отображает и выравнивает за один шаг — необходимо для вложенных преобразований. groupBy разделяет по ключу в Map. Это мощно для обработки данных и аналитических конвейеров.

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)

Сортировка

sorted/sortedDescending сортируют естественно. sortedBy/sortedByDescending сортируют по селектору ключа. Возвращают новые списки (неизменяемые). Для изменяемых списков используйте sort/sortBy (на месте). Используйте селекторы ключей для сортировки по конкретному полю.

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 ленивы — операции откладываются до терминальной операции (toList, sum, count). Избегает промежуточных коллекций для лучшей производительности на больших данных. Используйте asSequence() для много-шаговых конвейеров на больших коллекциях. Как 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 разделяет на два списка по предикату (возвращает Pair). chunked делит на списки фиксированного размера. windowed создаёт скользящие окна. Полезно для батчинга, пагинации и алгоритмов скользящего окна. Все возвращают новые коллекции.

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

Корутины и асинхронность

Launch (Fire и Forget)

launch запускает корутину, не возвращающую результат (fire-and-forget). delay не-блокирующий (в отличие от Thread.sleep). runBlocking мостит sync/async код (используйте в main/тестах). Корутины лёгкие — тысячи могут работать на нескольких потоках.

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 запускает корутину, возвращающую Deferred<T>. await() приостанавливает до готовности результата. Запустите несколько async до await для параллелизма. Используйте для параллельных вычислений, производящих результаты. Как Promise/Future в других языках.

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-функции

suspend-функции могут приостанавливаться и возобновляться без блокировки потоков. Могут вызываться только из корутин или других suspend-функций. coroutineScope предоставляет структурированную область (ждёт всех дочерних). Используйте для асинхронных API — делает асинхронный код похожим на синхронный.

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 (холодный поток)

Flow — холодный асинхронный поток Kotlin (как RxJava Observable). Значения производятся при сборе. Используйте операторы map/filter/reduce. emit производит, collect потребляет. Идеально для потоковой передачи данных, событий или пагинированных API. Горячие потоки используют 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 выбирают пул потоков: Main для UI, IO для сети/файлов (большой пул), Default для CPU-работы (количество ядер). withContext переключает контекст внутри корутины. Используйте правильный диспетчер для избежания блокировки UI или истощения пулов потоков.

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

Обработка ошибок и I/O

Try / Catch / Finally

try/catch/finally как в Java, но try — выражение, возвращающее значение. Нет проверяемых исключений — все непроверяемые. Ловите конкретные исключения для целевой обработки. Используйте finally для очистки. Предпочитайте Result или nullable-возврат для ожидаемых неудач.

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

Result<T> оборачивает успех или неудачу (как Try в Scala). runCatching преобразует исключения в Result. getOrNull/getOrElse для безопасного доступа. onSuccess/onFailure для обратных вызовов. Используйте для ожидаемых ошибок вместо исключений — чище функциональная обработка ошибок.

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

Пользовательские исключения

Пользовательские исключения наследуются от Exception (или подкласса). Передавайте сообщение для отладки. Ловите конкретные исключения до общих (порядок важен). Используйте исключения для подлинно исключительных случаев; для ожидаемых неудач предпочитайте Result или nullable-возврат.

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}")
}

Файловый I/O

Kotlin использует File Java с функциями расширения. writeText/readText для простых случаев. useLines потоково читает строки (авто-закрытие, эффективно по памяти). Для больших файлов используйте bufferedReader(). Всегда закрывайте ресурсы — используйте use { } блок для авто-закрытия.

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 — официальная JSON-библиотека Kotlin, типобезопасная на этапе компиляции, без рефлексии. Аннотируйте data-классы через @Serializable. encodeToString/decodeFromString для JSON. Используйте @SerialName для пользовательских имён ключей, @Optional для значений по умолчанию. Альтернатива: 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

Корутины: глубокое погружение

Структурированный параллелизм

Структурированный параллелизм связывает дочерние корутины с родительской областью — родитель не завершится, пока все дочерние не завершатся, а неудача дочерней отменяет сиблингов. Предпочитайте coroutineScope GlobalScope. Это предотвращает утечки корутин и делает отмену предсказуемой.

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
}

Отмена и кооперативная отмена

Отмена кооперативная — корутины должны проверять её в точках приостановки (delay, yield, await). Отменяемые suspend-функции вызывают CancellationException. Используйте ensureActive() или isActive для проверки. Не-suspend-ные CPU-циклы не реагируют на отмену, если не вызываете yield() или не проверяете 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

Обработка исключений

Необработанные исключения в launch распространяются к родителю (отменяя сиблингов), если не установлен CoroutineExceptionHandler. async откладывает исключения до await(). Используйте try/catch вокруг await. Обработчик работает только для не перехваченных исключений в launch — используйте SupervisorJob для изоляции неудач.

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")
    }
}

Супервизия (supervisorScope)

supervisorScope (и SupervisorJob) создаёт область, где неудача дочерней НЕ отменяет сиблингов — каждый дочерний завершается независимо. Используйте для независимых операций (например, несколько независимых API-вызовов). Обычный coroutineScope отменяет сиблингов при первой неудаче (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())
}

Каналы (CSP-стиль)

Channels передают значения между корутинами (как каналы Go). Rendezvous (capacity 0) синхронизирует отправителя и получателя; buffered допускает очередь. send приостанавливается при полноте, receive при пустоте. Всегда close() производителей. Для fan-out используйте BroadcastChannel или SharedFlow. Предпочитайте Flow для большинства потоковых нужд.

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: глубокое погружение

Операторы Flow

Операторы Flow холодные — выполняются только при сборе. map/filter/take подобны Stream/Sequence. transform самый гибкий (может испускать несколько значений на вход). flatMap-варианты обрабатывают вложенные flow: concat (последовательно), merge (параллельно), latest (отмена предыдущего). Выбирайте по потребностям в упорядочивании/параллелизме.

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 и параллелизм

buffer развязывает производителя и потребителя очередью фиксированной ёмкости — полезно, когда скорости производства и потребления различаются. conflate хранит только последнее значение (отбрасывает промежуточные) для обновлений UI/состояния. collectLatest отменяет предыдущего коллектора при поступлении нового значения — идеально для поиска-по-мере-ввода.

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 (горячие потоки)

StateFlow — горячий поток, хранящий одно актуальное значение — используйте для состояния UI (заменяет LiveData). Всегда имеет значение и conflate-ит. SharedFlow — горячий broadcast-поток (несколько коллекторов, без обязательного начального значения) — используйте для событий. SharedFlow с replay=1 ведёт себя как кэшированная шина событий.

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

catch обрабатывает восходящие исключения и может испускать резервные значения. Он перехватывает только исключения, испускаемые до него в цепочке. retry/retryWhen перепроводят flow при неудаче (полезно для сетевых вызовов). Для нисходящих исключений оберните collect в try/catch. Никогда не перехватывайте внутри построителя flow — позвольте распространиться.

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 и контекст

flowOn переключает диспетчер для восходящего flow (производитель + операторы выше него). Это важно, когда flow выполняет блокирующий I/O — оберните через flowOn(Dispatchers.IO). Нисходящий (collect) выполняется в контексте вызывающего. Несколько вызовов flowOn создают отдельные контексты для каждого сегмента.

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-классы и ADT

Основы sealed-классов

Sealed-классы ограничивают подклассы известным множеством (тот же файл/модуль). Компилятор знает все возможные типы, обеспечивая исчерпывающие when-выражения без else. Идеально для моделирования конечных состояний (Result, UiState, сетевые ответы). Сочетая с data-классами, формируют алгебраические типы данных.

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-интерфейсы (Kotlin 1.5+)

Sealed-интерфейсы (Kotlin 1.5+) расширяют sealed до интерфейсов, позволяя классу реализовать несколько sealed-типов — гибче sealed-классов. Применяется ограничение тот же-файл/тот же-модуль. Полезно для доменного моделирования, где тип принадлежит нескольким категориям.

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")
}

Исчерпывающий When

When, используемый как выражение (не оператор), компилятор требует все sealed-случаи — else не нужен. Добавление нового подкласса вызывает ошибки компиляции в необработанных when-местах, делая рефакторинг безопасным. Это киллер-функция sealed-классов для конечных автоматов и рендеринга UI.

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-моделирование (Result/Either)

Sealed-классы моделируют sum-типы (Either/Result) для railway-oriented программирования. fold обрабатывает обе ветви. В отличие от исключений, ошибки явны в сигнатуре типа. Arrow-kt предоставляет более богатые Either/Validated. Используйте для предсказуемой, компонуемой обработки ошибок без 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-классы с рекурсией

Sealed-классы могут быть рекурсивными, моделируя древовидные структуры (AST, JSON, выражения). Сопоставление шаблонов с when + рекурсия оценивает их элегантно. Это классическое функциональное ADT-использование — типобезопасно, исчерпывающе и дружелюбно к рефакторингу. Используется в компиляторах, парсерах и DSL конфигурации.

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

Делегированные свойства

Делегат lazy

lazy откладывает инициализацию до первого доступа и кэширует результат. По умолчанию потокобезопасный (double-checked locking). Используйте для дорогих ресурсов (конфиги, соединения с БД, синглтоны), которые могут не понадобиться. Передайте NONE mode для однопоточных контекстов во избежание накладных расходов синхронизации.

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 запускает колбэк после каждого изменения (логирование, побочные эффекты). vetoable может отклонить изменения, вернув false (валидация). Оба принимают начальное значение и лямбду. Используйте для реактивного состояния, валидации или запуска обновлений UI. Для более сложных сценариев используйте пользовательские делегаты.

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
}

Пользовательский делегат свойств

Пользовательские делегаты реализуют getValue (и setValue для var) через ReadOnlyProperty/ReadWriteProperty. Они инкапсулируют переиспользуемое поведение свойств (валидация, форматирование, кэширование, отображение БД). thisRef — владелец, prop — метаданные свойства. Мощно для ORM/serialization фреймворков.

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"
}

Свойства, backed Map

Делегирование в Map позволяет привязать свойства к динамическим ключам — полезно для разбора JSON, конфигов или строк ORM без шаблонности. Имя свойства должно совпадать с ключом map. Используйте MutableMap для записываемых свойств. Так работают kotlinx.serialization и многие ORM под капотом.

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

notNull() подобен lateinit, но для любого типа (включая примитивы) и работает с val/var. Вызывает исключение при доступе до установки. Для синглтонов предпочитайте object (энергично) или by lazy (лениво). lateinit — только для var в классах; делегат notNull гибче, но имеет небольшие накладные расходы.

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

Типобезопасные построители

Типобезопасные построители используют функциональные типы с получателем (лямбда с получателем) для создания вложенных DSL. Лямбда выполняется в контексте получателя (this), поэтому вы вызываете его методы напрямую. Так работают HTML DSL Kotlin, скрипты сборки Gradle и kotlinx.html — декларативно и проверяемо на этапе компиляции.

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 (контроль области)

@DslMarker предотвращает неявную утечку получателя — внутри вложенного DSL-блока можно вызывать только методы самого внутреннего получателя. Без него Kotlin позволил бы вызывать методы внешних получателей, что приводит к запутанным/багованным DSL. Аннотируйте все DSL-классы одной маркер-аннотацией.

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-функции

infix-функции обеспечивают естественно-языковой синтаксис (a to b, 1 until 10). Они должны быть членами или функциями расширения с одним параметром. Активно используются в DSL, фреймворках тестирования и математических библиотеках. Встроенные примеры: to (Pair), until/step (диапазоны), операции над коллекциями.

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. Перегружайте осмысленно — математические типы (Vec, Matrix, Money) выигрывают; произвольная перегрузка вредит читаемости. get/set обеспечивают индексацию; invoke делает объекты вызываемыми. Range-операторы (rangeTo, contains) обеспечивают циклы for.

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
}

Функциональные типы с получателем

Функциональные типы с получателем (A.(B) -> C) позволяют лямбдам обращаться к получателю как 'this'. Это основа DSL и scope-функций Kotlin (apply, run, with). apply возвращает получателя; run возвращает результат лямбды. Освойте это для построения выразительных, типобезопасных внутренних DSL.

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

Тестирование (JUnit 5, MockK, Turbine)

Основы JUnit 5

JUnit 5 (Jupiter) — стандартный фреймворк тестирования Kotlin. @Test помечает тесты; @BeforeEach/@AfterEach запускают per-test setup/teardown. Используйте backtick-имена для читаемости. @ParameterizedTest + @ValueSource запускают тест с несколькими входами. assertThrows проверяет исключения. @Disabled пропускает тесты.

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 (мокирование)

MockK — идиоматичная библиотека мокирования Kotlin (обрабатывает final-классы, корутины, функции расширения). every { } returns/throws заглушает; verify { } проверяет вызовы. Используйте relaxed = true для пропуска заглушения void-методов. MockK нативно поддерживает suspend-функции через coEvery/coVerify — необходимо для тестирования корутин.

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)

Turbine — стандартная библиотека тестирования Flow. test { } собирает в контролируемой области; awaitItem() утверждает следующую эмиссию, awaitComplete()/awaitError() проверяют завершение. Она правильно обрабатывает время и виртуальное время. Комбинируйте с runTest для корутино-дружественных, быстрых, детерминированных Flow-тестов.

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)
        }
    }
}

Тест корутин (runTest)

runTest использует виртуальное время — задержки пропускаются мгновенно, делая асинхронные тесты быстрыми и детерминированными. Используйте StandardTestDispatcher + advanceUntilIdle() для ручного управления. Внедряйте TestDispatcher в продакшен-код (вместо Dispatchers.Main) для тестируемости. Избегайте runBlocking в тестах — runTest современный способ.

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 и стили

Kotest предлагает выразительные утверждения (shouldBe, shouldContain) и несколько стилей спек (StringSpec, FunSpec, BehaviorSpec для BDD). Интегрируется с тестированием на основе свойств и имеет богатые матчеры. Выберите его вместо JUnit для Kotlin-идиоматичных, читаемых тестов. Может работать вместе с JUnit 5.

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

Функциональное программирование: глубокое погружение

Scope-функции (let, run, with, apply, also)

Scope-функции различаются получателем (this vs it) и возвращаемым значением (this vs result). apply/also возвращают получателя (цепочка, построители); let/run/with возвращают результат лямбды (преобразования). Используйте apply для конфигурации, let для null-проверок/преобразований, also для побочных эффектов. Не злоупотребляйте — читаемость прежде всего.

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 (ленивое вычисление)

Sequences вычисляются лениво — каждый элемент проходит весь конвейер до начала следующего (как Java Streams). Это избегает промежуточных коллекций и короткого замыкания (take/find). Используйте для больших наборов данных или много-шаговых конвейеров. Для маленьких списков энергичный List часто быстрее (меньше накладных расходов). generateSequence строит бесконечные потоки.

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-функции и reified

inline устраняет накладные расходы объекта/аллокации лямбды, инлайня байткод — критично для горячих циклов. Также обеспечивает не-локальные возвраты (return из внешней функции внутри лямбды). reified-параметры типа делают обобщённую информацию типа доступной во время выполнения (T::class, is T), но требуют inline. Используйте для типобезопасных помощников вроде 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>()

Функции высшего порядка

Функции высшего порядка принимают или возвращают функции. Они — основа функционального Kotlin — map, filter, fold, compose. Функциональные типы вроде (Int) -> Int — первого класса. compose строит конвейеры. Используйте их для абстрагирования паттернов, обеспечения переиспользования кода и написания декларативного кода. Помечайте inline для производительно-критических путей.

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 }
}

Рекурсия и tailrec

tailrec преобразует хвостово-рекурсивные функции (где рекурсия — последняя операция) в циклы, предотвращая переполнение стека. Рекурсивный вызов должен быть в хвостовой позиции — без ожидающих умножений/сложений. Используйте параметр-аккумулятор для создания хвостовой рекурсии. Существенно для функционального-стиля циклов на больших входах.

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

Обобщённые типы и вариантность

Обобщённые классы и функции

Обобщённые типы обеспечивают типобезопасный, переиспользуемый код. Классы используют <T>; функции объявляют <T> перед возвращаемым типом. Ограничения (T : Entity) сужают границу типа. В отличие от Java, обобщённые типы Kotlin reified для inline-функций и имеют вариантность на месте объявления, делая обобщённые API безопаснее и эргономичнее.

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 }

Вариантность (in/out)

Вариантность контролирует отношения подтипов обобщённых типов. out (ковариантный): Producer<Dog> является Producer<Animal> — безопасно, потому что только читаете T. in (контравариантный): Sink<Animal> является Sink<Dog> — безопасно, потому что только пишете T. Изменяемые коллекции инвариантны (чтение + запись). Используйте out/in для проектирования безопасных, гибких API.

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)

Вариантность на практике

Функциональные типы используют вариантность автоматически: параметры — 'in', возвращаемые типы — 'out'. Поэтому (Dog) -> Unit присваивается к (Animal) -> Unit. Принцип PECS (Producer Extends, Consumer Super) отображается в out/in Kotlin. Проектируйте обобщённые интерфейсы с out, когда они только производят T, in когда только потребляют T.

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

Проекции типов и звёздочная проекция

Проекции типов временно делают тип вариантным на месте использования. Array<out Any> означает 'Array какого-то подтипа Any, доступного для чтения'. Array<in Any> означает 'записываемый как Any'. Звёздочная проекция (*) означает 'какой-то неизвестный тип' — полезно, когда нужны только size/contains, а не тип элемента. Как wildcards Java.

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-параметры типа

reified сохраняет обобщённую информацию типа во время выполнения (обычно стирается на JVM). Требует inline (тип известен в каждом месте вызова). Обеспечивает проверки is T, T::class и filterIsInstance. Без reified пришлось бы передавать Class<T> параметр вручную. Ограничение: reified-типы нельзя использовать в не-inline функциях или как параметры типа класса.

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-безопасность: глубокое погружение

Типы, допускающие null

Kotlin различает типы, допускающие null (T?) и не-null (T), во время компиляции. ?. безопасный вызов возвращает null, если получатель null. ?: Elvis предоставляет значение по умолчанию. !! вызывает NPE (избегайте). Компилятор обеспечивает проверки null, устраняя NullPointerException в коде Kotlin.

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 выполняет блок, если значение не null (it = значение). run вызывает блок с объектом как получателем, возвращает результат. apply настраивает объект, возвращает объект. also выполняет побочные эффекты, возвращает объект. Эти scope-функции уменьшают проверки null и улучшают читаемость.

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 откладывает инициализацию не-null свойств. Нельзя использовать с примитивами или типами, допускающими null. Вызывает UninitializedPropertyAccessException при доступе до init. ::prop.isInitialized проверяет. Полезно для внедрения зависимостей и свойств, управляемых жизненным циклом.

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

Коллекции, допускающие null

Коллекции могут хранить элементы, допускающие null (List<String?>), или быть nullable (List<String>?). filterNotNull удаляет null. firstOrNull возвращает null вместо исключения. Обрабатывайте элементы, допускающие null, безопасными вызовами. Будьте явны о null-допустимости в типах коллекций.

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 }

Платформенные типы

Платформенные типы возникают из Java interop, где null-допустимость неизвестна. Kotlin не может обеспечить null-безопасность для них. Всегда объявляйте типы, допускающие null, явно для возвращаемых значений Java. Используйте аннотации @Nullable/@NotNull в Java. Аннотации JSR-305 помогают Kotlin вывести null-допустимость.

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

Построитель с лямбда-получателем

Лямбда с получателем (T.() -> Unit) обеспечивает DSL-синтаксис. Внутри лямбды this — объект-получатель. Методы можно вызывать без квалификации. Так Kotlin строит типобезопасные DSL вроде Gradle, HTML и SQL-построителей.

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

HTML DSL использует вложенные построители с лямбда-получателями. Каждый тег — функция, создающая дочерний построитель. Лямбда настраивает дочерний. Производит типобезопасный, компонуемый HTML. kotlinx.html — реальная реализация. Тот же паттерн работает для любой иерархической структуры.

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 предотвращает неявный доступ к получателю внешних областей. Без него доступны методы и HTML, и Body, вызывая путаницу. Аннотация ограничивает доступ до самого внутреннего получателя. Делает DSL типобезопасными и однозначными. Существенно для сложных DSL.

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

Gradle Kotlin DSL использует тот же паттерн построителя. plugins, dependencies — функции с лямбда-получателями. implementation, testImplementation — функции конфигурации зависимостей. Типобезопасно: компилятор проверяет имена функций и типы параметров. Гораздо лучше Groovy для рефакторинга.

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 (и Exposed) предоставляют типобезопасные SQL DSL. Сравнения колонок типизированы. Нельзя сравнить строковую колонку с int. DSL генерирует SQL. Предотвращает SQL-инъекции и ошибки типов. Тот же паттерн применим к любому доменно-специфичному языку.

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

Коллекции

Операции со списками

Коллекции Kotlin имеют богатые функциональные API. map, filter, reduce стандартны. groupBy разделяет по ключу. chunked делит на списки фиксированного размера. windowed создаёт скользящие окна. Все возвращают новые коллекции. Используйте asSequence() для ленивого вычисления.

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 — ленивая коллекция (как Java Stream). Операции вычисляются по требованию. Нет промежуточных коллекций. Эффективно для больших коллекций с цепочечными операциями. Используйте asSequence() для преобразования. Принуждайте через toList(), toSet() и т.д.

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

Деструктуризация

Деструктуризация разделяет объекты на переменные. Работает с Pair, Triple, data-классами и элементами Map. Функции componentN() обеспечивают это. withIndex() парит индекс и значение. Полезно для множественных возвращаемых значений и итерации. Data-классы автогенерируют componentN.

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)

Изменяемые vs неизменяемые

Kotlin имеет и изменяемые (MutableList), и неизменяемые (List) коллекции. Предпочитайте неизменяемые для безопасности. toList() создаёт неизменяемую копию. toMutableList() создаёт изменяемую копию. Неизменяемые интерфейсы не раскрывают методы мутации, предотвращая случайную модификацию.

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 преобразуют в map. partition разделяет на два списка по предикату. flatten объединяет вложенные списки. flatMap отображает и выравнивает. Они заменяют многословные циклы декларативными выражениями. Все возвращают новые коллекции.

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

Распространённые подводные камни

== vs ===

Kotlin == вызывает equals (значимое равенство), в отличие от Java. === проверяет равенство ссылок. Используйте == для значимого сравнения. === нужен редко. Для Integer значения -128 до 127 кэшируются, поэтому === может быть true или false. Всегда используйте == для значений.

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 не имеет static-членов. companion object содержит "static" методы и константы. const val — истинная константа времени компиляции. companion object — singleton-экземпляр. @JvmStatic делает методы вызываемыми как static из Java. Используйте top-level функции для настоящих static.

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

Copy data-класса

Data-классы автогенерируют copy(), создающий модифицированную копию. Оригинал не изменён (неизменяемый). Меняются только указанные поля. Полезно для обновлений. Сочетая с деструктуризацией, data-классы мощны для моделирования данных. Избегайте var в data-классах для неизменяемости.

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-класса

Sealed-классы ограничивают подтипы тем же файлом/пакетом. when на sealed-классах исчерпывающий: компилятор предупреждает, если ветка пропущена. else не нужен, если все случаи покрыты. Добавление нового подтипа вызывает предупреждения везде. Идеально для конечных автоматов и результатов.

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

Расширение vs член

Функции-члены всегда имеют приоритет над функциями расширения с той же сигнатурой. Расширения разрешаются во время компиляции (статическая диспетчеризация), члены во время выполнения (динамическая диспетчеризация). Расширения не могут переопределять члены. Используйте расширения для утилитарных функций, не для полиморфизма.

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

Общий код

Kotlin Multiplatform (KMP) разделяет код между платформами. expect/actual объявляет платформо-специфичные реализации. commonMain содержит разделяемый код. Платформо-специфичные source set реализуют actual. Разделяйте бизнес-логику, сохраняйте UI нативным. Gradle настраивает target-ы.

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()}"
}

Разделяемый модуль

Multiplatform-проекты используют kotlin-блок в Gradle. Определите target-ы (android, ios). commonMain имеет разделяемые зависимости. Платформенные source set могут иметь платформо-специфичные зависимости. iOS использует Kotlin/Native для прямой компиляции. Разделяйте логику, не 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") }
        }
    }
}

Сетевое взаимодействие (Ktor)

Ktor — multiplatform HTTP-клиент. API общий, движок платформо-специфичен. Android использует OkHttp/Android engine. iOS использует Darwin. Корутины работают между платформами. Разделяйте сетевое взаимодействие, БД (SQLDelight) и бизнес-логику.

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 генерирует типобезопасный Kotlin из SQL. .sq-файлы содержат SQL с именованными запросами. Генерирует типобезопасные объекты запросов. Работает между платформами (Android, iOS, JVM). SQL — источник истины. Миграции схемы отслеживаются. Альтернатива Room для 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 расширяет Jetpack Compose на iOS, Desktop и Web. Разделяйте UI-код между платформами. Тот же @Composable API. Платформо-специфичные точки входа. Всё ещё экспериментально для iOS. Уменьшает дублирование UI. Gradle-плагин: 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

Тестирование Kotlin

JUnit 5

JUnit 5 — стандартный фреймворк тестирования. Kotlin допускает backtick-имена тестов для читаемости. assertEquals, assertThrows — распространённые утверждения. @BeforeEach, @AfterEach для setup/teardown. @ParameterizedTest для data-driven тестов. Используйте kotlin.test для multiplatform.

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 — Kotlin-нативная библиотека мокирования. Поддерживает final-классы и функции расширения. every заглушает, verify проверяет вызовы. @MockK создаёт моки, @InjectMockKs внедряет их. coEvery/coVerify для корутин. Лучшая поддержка Kotlin, чем 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 — Kotlin-first фреймворк тестирования. Несколько стилей: StringSpec, BehaviorSpec, FunSpec. shouldBe — fluent-утверждение. Тестирование свойств с Arb. Поддерживает data-driven тесты. Интегрируется со Spring и Ktor. Более Kotlin-идиоматичен, чем 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) }
    }
})

Тестирование корутин

runTest из kotlinx-coroutines-test предоставляет виртуальное время. Задержки пропускаются мгновенно. advanceUntilIdle запускает ожидающие корутины. Гораздо быстрее реального времени. Используйте TestDispatcher для тонкого контроля. Библиотека Turbine тестирует эмиссии Flow.

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)

Turbine тестирует эмиссии Flow. awaitItem получает следующую эмиссию. awaitComplete утверждает завершение. awaitError утверждает ошибку. test-блок приостанавливается до завершения flow. Гораздо чище, чем ручной сбор. Существенно для тестирования Flow.

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

Корутины: глубокое погружение

Область корутины

CoroutineScope определяет время жизни корутин. viewModelScope авто-отменяется при очистке VM. SupervisorJob предотвращает отмену сиблингов при неудаче дочерней. Пользовательские области требуют явной отмены. Области распространяют отмену к дочерним. Никогда не используйте GlobalScope в продакшене (нельзя отменить).

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 маршрутизируют корутины в пулы потоков. Main: UI-поток (Android). IO: блокирующий I/O (64+ потока). Default: CPU-работа (количество CPU-потоков). Unconfined: поток вызывающего (продвинуто). withContext переключает диспетчер временно. Выбор правильного диспетчера улучшает производительность и предотвращает замерзание UI.

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

Операторы Flow: map преобразует, filter выбирает, flatMapMerge/flatMapConcat компонуют flow. Холодные flow стартуют заново для каждого коллектора. StateFlow хранит значение (как LiveData). SharedFlow вещает нескольким коллекторам. stateIn преобразует холодный в горячий. buffer/conflate контролируют 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

Обработка исключений

CoroutineExceptionHandler перехватывает не перехваченные исключения в launch. Исключения async распространяются до await. SupervisorJob изолирует неудачи дочерних. CancellationException особый: перегенерируется, не ловится catch. Никогда не проглатывайте CancellationException. Используйте try/finally или use() для очистки. Отмена распространяется через suspend-вызовы.

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
}

Каналы

Каналы обеспечивают coroutine-to-coroutine взаимодействие. send приостанавливается при полноте, receive при пустоте. capacity: buffered (RENDEZVOUS=0, UNLIMITED, CONFLATED). close() сигнализирует завершение. produce создаёт корутину-производителя. Channel горячий: значения потребляются один раз. Предпочитайте Flow для большинства случаев.

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.