Основы
Переменные и типы
Предпочитайте val (неизменяемое) var (изменяемое). Kotlin выводит типы, но допускает явные аннотации. Используйте 'is' для проверок типа (smart-cast автоматически). Всё — объект, нет примитивов в синтаксисе 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() для чистого форматирования. Строковые шаблоны делают построение строк лаконичным и читаемым без строк форматирования.
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.
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 из разбора.
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.
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Строки
Распространённые строковые методы
Строки Kotlin имеют богатые функции расширения из стандартной библиотеки. Большинство возвращают новые строки (неизменяемые). Используйте их вместо ручных циклов. Функции вроде uppercase() учитывают локаль — используйте uppercase(Locale.ROOT) для согласованности.
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 в коде.
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 для эффективности.
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-варианты для пользовательского ввода или ненадёжных данных.
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 — частичное. Используйте строки в тройных кавычках для шаблонов во избежание двойного экранирования обратных слэшей.
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Структуры данных
Список
listOf создаёт неизменяемые списки, mutableListOf для изменяемых. Списки индексируются с нуля. Используйте first()/last() для концов (вызывают исключение при пустом), firstOrNull() для безопасности. contains проверяет принадлежность. Предпочитайте неизменяемые списки, если не нужно изменять.
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)) // 1Map
mapOf создаёт неизменяемые map, mutableMapOf для изменяемых. Инфикс 'to' создаёт пары. Доступ [] возвращает nullable V? (null если отсутствует). Используйте getOrDefault или getValue (вызывает исключение, если отсутствует). Итерируйте с деструктуризацией (k, v).
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 сохраняет порядок вставки.
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 для большинства случаев.
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-классы — они яснее и поддерживаемее.
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())Поток управления
If / Else (выражение)
В Kotlin if/else — выражение, возвращающее значение. Это заменяет тернарный оператор. Обе ветви должны возвращать совместимые типы. Используйте для лаконичного условного присваивания. Как оператор (без else), возвращает Unit.
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 typesWhen (Switch)
when — мощный switch Kotlin — поддерживает литералы, диапазоны (in), отрицание (!in), несколько значений (запятая) и is-проверки. Должно быть исчерпывающим при использовании как выражение с sealed-типами. Без субъекта — более чистая if/else цепочка.
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. Эти диапазоны лаконичны и читаемы для счётных циклов.
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 для циклов, управляемых условием, где счётчик неизвестен.
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 possibleBreak и Continue (метки)
continue пропускает к следующей итерации, break выходит из цикла. Метки (@outer) обеспечивают выход из вложенных циклов — определите через label@ перед циклом, break/continue с @label. Используйте умеренно; рефакторинг в функции часто чище.
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")
}
}Функции и лямбды
Определение функции
Функции используют fun name(params): ReturnType. Тело-выражение (= expr) для одиночных выражений. Поддерживаются параметры по умолчанию и именованные аргументы. Возврат Unit = только побочный эффект. Параметры по умолчанию уменьшают необходимость перегрузки.
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.
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. Разрешаются во время компил яции (без динамической диспетчеризации).
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 typeInline и Infix
inline копирует тело функции в места вызова (устраняет накладные расходы лямбды). Используйте для функций высшего порядка в горячих путях. infix включает вызовы функций без точки/скобок (a op b) — используйте для DSL и читаемых операций вроде 'to', 'in', 'and'.
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") // abababVararg и Spread
vararg принимает переменные аргументы (собираются в массив). * разворачивает массив в vararg. Именованные параметры могут следовать за vararg. Используйте для гибких API вроде listOf(), printf-подобных функций. Оператор spread — эквивалент spread JS в 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"))Классы и ООП
Класс и конструктор
Первичный конструктор в заголовке класса. Параметры val/var становятся свойствами (с getter/setter). Без val/var они просто параметры конструктора (недоступны). Блок init выполняется при конструировании. Kotlin генерирует getter/setter автоматически.
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 propertyData-класс
data class автогенерирует equals, hashCode, toString, copy и componentN (для деструктуризации). Используйте для держателей данных. Должен иметь хотя бы один val/var параметр. copy() создаёт модифицированный клон — отлично для неизменяемых обновлений. Деструктурируйте через val (a, b) = point.
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, обеспечивает безопасное, проверяемое на этапе компиляции сопоставление шаблонов.
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 casesObject и Companion
object объявляет singleton (один экземпляр, лениво инициализируется). companion object внутри класса содержит 'static' члены (доступ через ClassName.member). Используйте object для синглтонов, companion для фабричных методов и констант. Companion может реализовывать интерфейсы.
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 для частичной реализации. Предпочитайте композицию наследованию.
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"
}Null-безопасность и scope-функции
Безопасные вызовы и Elvis
?. безопасный вызов (возвращает null, если получатель null), ?: elvis (предоставляет значение по умолчанию). Цепочка ?. для глубокого доступа (user?.address?.city). Используйте ?: return/throw для ранних выходов. Это делает обработку null лаконичной и безопасной — без явных проверок null.
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?.citylet (проверка null)
let выполняет блок только если значение не null. 'it' — не-null значение внутри блока. Используйте для null-защищённых операций вместо if-null проверок. Распространённый паттерн: value?.let { ... } для безопасной обработки. Возвращает результат блока.
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-цепочки.
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). Используйте для группировки операций над одним объектом.
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-проверок. Комбинируйте с ?: для значений по умолчанию. Элегантно для конвейеров валидации.
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)Коллекции и функциональное
Map / Filter / Fold
map преобразует, filter выбирает, reduce/fold агрегирует. 'it' — неявный элемент. fold принимает seed; reduce не делает (вызывает исключение при пустом). Это ядро функциональной обработки коллекций — используйте вместо циклов для ясности.
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, 5FlatMap и GroupBy
flatten удаляет один уровень вложенности. flatMap отображает и выравнивает за один шаг — необходимо для вложенных преобразований. groupBy разделяет по ключу в Map. Это мощно для обработки данных и аналитических конвейеров.
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 (на месте). Используйте селекторы ключей для сортировки по конкретному полю.
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.
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 создаёт скользящие окна. Полезно для батчинга, пагинации и алгоритмов скользящего окна. Все возвращают новые коллекции.
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]]Корутины и асинхронность
Launch (Fire и Forget)
launch запускает корутину, не возвращающую результат (fire-and-forget). delay не-блокирующий (в от личие от Thread.sleep). runBlocking мостит sync/async код (используйте в main/тестах). Корутины лёгкие — тысячи могут работать на нескольких потоках.
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 1sAsync и Await
async запускает корутину, возвращающую Deferred<T>. await() приостанавливает до готовности результата. Запустите несколько async до await для параллелизма. Используйте для параллельных вычислений, производящих результаты. Как Promise/Future в других языках.
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 — делает асинхронный код похожим на синхронный.
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 coroutinesFlow (холодный поток)
Flow — холодный асинхронный поток Kotlin (как RxJava Observable). Значения производятся при сборе. Используйте операторы map/filter/reduce. emit производит, collect потребляет. Идеально для потоковой передачи данных, событий или пагинированных API. Горячие потоки используют SharedFlow/StateFlow.
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 или истощения пулов потоков.
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() }
}Обработка ошибок и I/O
Try / Catch / Finally
try/catch/finally как в Java, но try — выражение, возвращающее значение. Нет проверяемых исключений — все непроверяемые. Ловите конкретные исключения для целевой обработки. Используйте finally для очистки. Предпочитайте Result или nullable-возврат для ожидаемых неудач.
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 для обратных вызовов. Используйте для ожидаемых ошибок вместо исключений — чище функциональная обработка ошибок.
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-возврат.
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 { } блок для авто-закрытия.
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.
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Корутины: глубокое погружение
Структурированный параллелизм
Структурированный параллелизм связывает дочерние корутины с родительской областью — родитель не завершится, пока все дочерние не завершатся, а неудача дочерней отменяет сиблингов. Предпочитайте coroutineScope GlobalScope. Это предотвращает утечки корутин и делает отмену предсказуемой.
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.
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 для изоляции неудач.
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).
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 для большинства потоковых нужд.
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
}Flow: глубокое погружение
Операторы Flow
Операторы Flow холодные — выполняются только при сборе. map/filter/take подобны Stream/Sequence. transform самый гибкий (может испускать несколько значений на вход). flatMap-варианты обрабатывают вложенные flow: concat (последовательно), merge (параллельно), latest (отмена предыдущего). Выбирайте по потребностям в упорядочивании/параллелизме.
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 отменяет предыдущего коллектора при поступлении нового значения — идеально для поиска-по-мере-ввода.
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 ведёт себя как кэшированная шина событий.
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 — позвольте распространиться.
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 создают отдельные контексты для каждого сегмента.
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) }
}Sealed-классы и ADT
Основы sealed-классов
Sealed-классы ограничивают подклассы известным множеством (тот же файл/модуль). Компилятор знает все возможные типы, обеспечивая исчерпывающие when-выражения без else. Идеально для моделирования конечных состояний (Result, UiState, сетевые ответы). Сочетая с data-классами, формируют алгебраические типы данных.
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/moduleSealed-интерфейсы (Kotlin 1.5+)
Sealed-интерфейсы (Kotlin 1.5+) расширяют sealed до интерфейсов, позволяя классу реализовать несколько sealed-типов — гибче sealed-классов. Применяется ограничение тот же-файл/тот же-модуль. Полезно для доменного моделирования, где тип принадлежит нескольким категориям.
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.
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 itADT-моделирование (Result/Either)
Sealed-классы моделируют sum-типы (Either/Result) для railway-oriented программирования. fold обрабатывает обе ветви. В отличие от исключений, ошибки явны в сигнатуре типа. Arrow-kt предоставляет более богатые Either/Validated. Используйте для предсказуемой, компонуемой обработки ошибок без try/catch.