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