🟧 Kotlin

Полный справочник: типы и null-безопасность, функции и лямбды, классы, data и sealed, коллекции, корутины и Flow, DSL, Java-интероп, Gradle и экосистема.

Шпаргалки · Программирование · #kotlin #jvm #android #backend

Запуск и окружение

kotlinc hello.kt -include-runtime -d hello.jar && java -jar hello.jar
kotlin script.kts            gradle build           ./gradlew test

IDE: IntelliJ IDEA, Android Studio. Сборка: Gradle (Kotlin DSL build.gradle.kts), Maven. Платформы: JVM, Android, Kotlin Multiplatform (iOS, Web, Native), Kotlin/JS и Wasm. Линтеры: ktlint, detekt.

Минимальная программа

fun main() {
    println("Привет")
}
fun main(args: Array<String>) { }

Точка с запятой не нужна. Пакеты: package com.example, import kotlin.math.*.

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

val n: Int = 1            // val: неизменяемая ссылка
var s = "a"               // var: изменяемая, тип выводится
const val MAX = 10         // константа времени компиляции (на верхнем уровне)
lateinit var service: Service
val lazyVal by lazy { heavy() }
Тип Примеры
Int, Long, Short, Byte 1, 1L
Double, Float 1.5, 1.5f
Boolean, Char, String true, 'a', "s"
Any, Unit, Nothing корень типов, «ничего полезного», «никогда не вернётся»
List, Set, Map, Array коллекции
1_000_000; 0xFF; 3.toDouble(); "42".toInt(); "x".toIntOrNull(); 7 / 2 // 3 ; 7 / 2.0 // 3.5
val big = 10_000_000_000L;   n as Long?  /  n is Int

Нет неявного расширения: val l: Long = n не компилируется, пишите n.toLong().

Null-безопасность

var a: String = "x"          // не может быть null
var b: String? = null         // может
b?.length                     // безопасный вызов: null, если b == null
b?.length ?: 0                // элвис: значение по умолчанию
b!!.length                    // «я уверен», бросит NPE (избегайте)
b?.let { println(it) }        // выполнить, если не null
if (b != null) b.length        // умное приведение (smart cast)
requireNotNull(b) { "b обязателен" };  checkNotNull(b)

Строки

val name = "Аня"
"Привет, $name! Длина: ${name.length}"
"""
    Многострочная
    строка
""".trimIndent()
name.uppercase(); name.lowercase(); name.trim(); name.replace("а", "я"); name.split(","); name.substring(1, 3)
name.startsWith("А"); name.contains("н"); name.isBlank(); name.padStart(5, '0'); name.reversed(); name.take(2); name.drop(1)
"%.2f".format(3.14159); buildString { append("a"); appendLine("b") }

Управление

if (x > 0) "плюс" else "минус"              // if — выражение
val r = when (x) { 0 -> "ноль"; in 1..9 -> "цифра"; is Int -> "int"; else -> "другое" }
when { x < 0 -> ...; x == 0 -> ...; else -> ... }
for (i in 0 until 3) {}   for (i in 10 downTo 0 step 2) {}   for ((i, v) in list.withIndex()) {}   for (c in "abc") {}
while (cond) {}  do {} while (cond)
repeat(3) { println(it) }
loop@ for (i in 1..3) { for (j in 1..3) { if (j == 2) continue@loop } }

Диапазоны: 1..5 (включительно), 1 until 5, 5 downTo 1, x in 1..5.

Функции

fun add(a: Int, b: Int = 0): Int = a + b
fun greet(name: String, greeting: String = "Привет") = "$greeting, $name"
greet(greeting = "Hi", name = "Аня")                         // именованные аргументы
fun sum(vararg nums: Int) = nums.sum()
val sq: (Int) -> Int = { x -> x * x }
list.map { it * 2 }                                           // it: единственный параметр
fun String.shout() = uppercase() + "!"                        // функция-расширение
inline fun <T> measure(block: () -> T): T { ... }
fun <T : Comparable<T>> maxOf(a: T, b: T) = if (a > b) a else b
infix fun Int.pow(e: Int): Int = ...                          // 2 pow 3
operator fun Point.plus(o: Point) = Point(x + o.x, y + o.y)

Функции области видимости

Функция Объект как Результат
let it результат лямбды
run this результат лямбды
with(obj) this результат лямбды
apply this сам объект (настройка)
also it сам объект (побочные эффекты)
val user = User().apply { name = "Аня"; age = 30 }
user.also { log(it) }.let { save(it) }

Классы

class User(val id: Int, var name: String, private val email: String? = null) {
    init { require(name.isNotBlank()) }
    val label get() = "@$name"
    fun hello() = "Привет, $name"
    companion object { fun create(n: String) = User(0, n) }
}
open class Base { open fun run() {} }
class Child : Base() { override fun run() { super.run() } }
interface Shape { val sides: Int; fun area(): Double; fun info() = "area=${area()}" }
abstract class Animal { abstract fun voice() }
object Config { val url = "x" }                                // синглтон

data class Point(val x: Int, val y: Int)                        // equals, hashCode, toString, copy, componentN
val p2 = p.copy(x = 5);  val (px, py) = p
enum class Role(val level: Int) { USER(1), ADMIN(2) }
@JvmInline value class UserId(val value: Long)                  // обёртка без накладных расходов

sealed interface Result<out T> {                                // закрытая иерархия
    data class Ok<T>(val value: T) : Result<T>
    data class Err(val error: Throwable) : Result<Nothing>
}
fun handle(r: Result<Int>) = when (r) { is Result.Ok -> r.value; is Result.Err -> 0 }   // исчерпывающий when

По умолчанию классы закрыты (final): наследование требует open. Видимость: public (по умолчанию), internal, protected, private. Делегирование: class A(b: B) : B by b, by lazy, by Delegates.observable.

Коллекции

val l = listOf(3, 1, 2)            val ml = mutableListOf(1)        val m = mapOf("a" to 1)        val mm = mutableMapOf<String, Int>()
val s = setOf(1, 2);               val arr = arrayOf(1, 2);         val ia = intArrayOf(1, 2)
l.map { it * 2 }.filter { it > 2 }.sortedBy { -it }.take(2)
l.first(); l.firstOrNull { it > 5 }; l.last(); l.getOrNull(10); l.find { }; l.any { }; l.all { }; l.none { }; l.count { }
l.sum(); l.max(); l.min(); l.average(); l.sumOf { it.price }; l.maxByOrNull { it.age }
l.groupBy { it % 2 }; l.partition { it > 1 }; l.associateBy { it.id }; l.associate { it to it * 2 }; l.zip(other); l.chunked(2); l.windowed(3)
l.flatMap { listOf(it, it) }; l.distinct(); l.reduce { a, b -> a + b }; l.fold(0) { acc, x -> acc + x }
m.getOrDefault("z", 0); m["a"]; m.getValue("a"); m.mapValues { (_, v) -> v + 1 }; m.filterKeys { }; mm.getOrPut("k") { 0 }; mm["k"] = 1
l.asSequence().map { }.filter { }.toList()                      // ленивые вычисления для больших данных

Корутины

import kotlinx.coroutines.*

suspend fun load(id: Int): String { delay(100); return "item$id" }

fun main() = runBlocking {
    val a = async { load(1) }              // параллельно
    val b = async { load(2) }
    println(a.await() + b.await())
    launch { delay(50); println("фон") }                // fire-and-forget
    withContext(Dispatchers.IO) { readFile() }           // переключение потока
    withTimeout(1000) { load(3) }
    coroutineScope { launch { } ; launch { } }           // дождаться дочерних
}
Dispatcher Для чего
Dispatchers.Default вычисления на CPU
Dispatchers.IO ввод-вывод, сеть, диск
Dispatchers.Main UI (Android, Compose)

Структурированная конкурентность: корутина живёт в CoroutineScope, отмена родителя отменяет детей. Не используйте GlobalScope. Не глотайте CancellationException.

Flow

fun ticker(): Flow<Int> = flow { var i = 0; while (true) { emit(i++); delay(1000) } }
ticker().map { it * 2 }.filter { it > 2 }.take(5).collect { println(it) }
val state = MutableStateFlow(0);  state.value = 1             // состояние
val events = MutableSharedFlow<Event>()                         // события

Исключения

try { Integer.parseInt(s) } catch (e: NumberFormatException) { -1 } finally { }
val n = runCatching { s.toInt() }.getOrDefault(0)
throw IllegalStateException("нельзя")        require(x > 0) { "x > 0" }        check(initialized)

Проверяемых исключений нет (всё как RuntimeException). Ошибки как значения: sealed Result, kotlin.Result, библиотека Arrow.

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

fun html(block: Html.() -> Unit) = Html().apply(block)
html { body { div { +"текст" } } }

Так сделаны Gradle Kotlin DSL, Ktor, Compose, kotlinx.html, Exposed.

Файлы и JSON

File("a.txt").readText(); File("a.txt").writeText("x"); File("a.txt").forEachLine { }; Path("a").exists()
@Serializable data class User(val id: Int, val name: String)        // kotlinx.serialization
Json.encodeToString(user);  Json.decodeFromString<User>(text)

Интероп с Java

Kotlin вызывает Java и наоборот. Аннотации: @JvmStatic, @JvmOverloads, @JvmField, @Throws. Платформенные типы Java (String!) не проверяются на null: аннотации @Nullable / @NotNull помогают.

Тесты

class CalcTest {
    @Test fun adds() = assertEquals(3, add(1, 2))
    @Test fun suspending() = runTest { assertEquals("item1", load(1)) }          // kotlinx-coroutines-test
}
val repo = mockk<Repo>(); every { repo.find(1) } returns user; verify { repo.find(1) }

JUnit 5, kotlin.test, Kotest, MockK, Turbine (Flow), Testcontainers.

Подводные камни

  • !! скрывает ошибки, предпочитайте ?: и requireNotNull.
  • Блокировка потока внутри корутины (Thread.sleep, блокирующий ввод-вывод вне Dispatchers.IO).
  • Изменяемые коллекции наружу: отдавайте List, храните MutableList.
  • data class с var в хэше: ломает HashSet.
  • Забытый open / override.
  • Бесконечные Flow без отмены.

Экосистема по задачам

Задача Что взять
Сервер Ktor, Spring Boot, Micronaut, Http4k
Android UI Jetpack Compose, ViewModel, Navigation, Hilt
Мультиплатформа Kotlin Multiplatform, Compose Multiplatform
БД Exposed, Room, SQLDelight, jOOQ
Сеть Ktor Client, Retrofit, OkHttp
JSON kotlinx.serialization, Moshi, Jackson
DI Koin, Hilt, Kodein
Корутины kotlinx.coroutines
Функциональные приёмы Arrow
Тесты Kotest, MockK, JUnit 5