Перейти к основному содержимому

5.09. Справочник по Kotlin

Разработчику Архитектору

Справочник по Kotlin

Основы языка

Структура программы

Kotlin-программа состоит из файлов с расширением .kt. Каждый файл может содержать:

  • Декларации пакетов (package)
  • Импорты (import)
  • Топ-левел функции и свойства
  • Классы, интерфейсы, объекты, перечисления, аннотации
  • Псевдонимы типов (typealias)

Пример минимальной программы:

fun main() {
println("Hello, Kotlin!")
}

Функция main является точкой входа. Она может принимать аргументы командной строки:

fun main(args: Array<String>) {
println("Arguments: ${args.joinToString()}")
}

Пакеты и импорты

Все исходные файлы начинаются с объявления пакета:

package com.example.myapp

Если пакет не указан, элементы размещаются в корневом пространстве имён.

Импорты указываются после объявления пакета:

import kotlin.system.exitProcess
import java.util.*
import kotlin.collections.List as KotlinList

Поддерживается переименование импорта через ключевое слово as.

Комментарии

Kotlin поддерживает три типа комментариев:

  • Однострочный: // комментарий
  • Многострочный: /* комментарий */
  • Документирующий (KDoc): /** документация */

KDoc используется для генерации документации и поддерживает теги: @param, @return, @throws, @see, @since.

Переменные и константы

Kotlin различает изменяемые и неизменяемые ссылки:

  • val — неизменяемая ссылка (аналог final в Java)
  • var — изменяемая ссылка
val name: String = "Kotlin"
var age: Int = 10

Тип может быть выведен автоматически:

val message = "Hello" // тип String выводится
var count = 42 // тип Int выводится

После инициализации val нельзя переназначить. Значение var можно менять, но только на значение того же типа.

Типы данных

Kotlin — язык со строгой статической типизацией. Все типы делятся на:

  • Числовые
  • Булевы
  • Символьные
  • Строковые
  • Массивы
  • Коллекции
  • Функциональные типы
  • Nullable и non-nullable типы

Числовые типы

ТипРазмер (бит)Диапазон значений
Byte8-128 до 127
Short16-32768 до 32767
Int32-2³¹ до 2³¹−1
Long64-2⁶³ до 2⁶³−1
Float32IEEE 754
Double64IEEE 754

Литералы:

val b: Byte = 127
val s: Short = 32767
val i = 1_000_000 // подчёркивания допустимы
val l = 1_000_000_000L // суффикс L для Long
val f = 3.14f // суффикс f для Float
val d = 3.14 // Double по умолчанию

Булев тип

val isActive = true
val isReady = false

Операторы: &&, ||, !

Символы

Тип Char представляет один символ в одинарных кавычках:

val letter = 'A'
val digit = '5'

Char не является числовым типом и не может использоваться в арифметических операциях без явного преобразования.

Строки

Строки неизменяемы. Объявляются в двойных кавычках:

val greeting = "Hello"

Поддержка интерполяции:

val name = "Timur"
println("Hello, $name!") // Hello, Timur!
println("Length: ${name.length}") // Length: 5

Многострочные строки в тройных кавычках:

val text = """
Line 1
Line 2
Line 3
""".trimIndent()

Метод trimIndent() удаляет общий отступ.

Массивы

Массивы создаются с помощью фабричных функций:

val numbers = arrayOf(1, 2, 3)
val intArray = intArrayOf(1, 2, 3) // примитивный массив
val chars = charArrayOf('a', 'b')

Доступ к элементам по индексу:

println(numbers[0]) // 1
numbers[0] = 10

Размер массива: numbers.size

Nullable и non-nullable типы

Kotlin различает nullable и non-nullable типы на уровне системы типов:

  • String — не может быть null
  • String? — может быть null

Присвоение null требует явного указания nullable-типа:

val name: String? = null

Операторы безопасного вызова и элвиса:

val length = name?.length ?: 0

Оператор !! принудительно разыменовывает nullable-значение и выбрасывает исключение при null.

Условные конструкции

if-выражение

if в Kotlin — выражение, возвращающее значение:

val max = if (a > b) a else b

Полная форма:

val result = if (score > 90) {
"Excellent"
} else if (score > 70) {
"Good"
} else {
"Needs improvement"
}

when-выражение

Аналог switch, но мощнее:

val response = when (code) {
200 -> "OK"
404 -> "Not Found"
in 500..599 -> "Server Error"
else -> "Unknown"
}

Поддерживает:

  • Сравнение по значению
  • Диапазоны (in 1..10)
  • Проверку типов (is String)
  • Условия (x % 2 == 0)
  • Ветку else как обязательную при неисчерпывающем перечислении

when может использоваться без аргумента:

when {
x < 0 -> println("Negative")
x == 0 -> println("Zero")
else -> println("Positive")
}

Циклы

for

Итерация по диапазону:

for (i in 1..5) println(i)
for (i in 5 downTo 1) println(i)
for (i in 1 until 5) println(i) // 1..4
for (i in 1..10 step 2) println(i) // 1, 3, 5, 7, 9

Итерация по коллекции:

for (item in list) { ... }
for ((index, value) in list.withIndex()) { ... }

while и do-while

while (condition) { ... }
do { ... } while (condition)

Функции

Функции объявляются ключевым словом fun:

fun greet(name: String): String {
return "Hello, $name!"
}

Сокращённая форма для однострочных функций:

fun square(x: Int) = x * x

Параметры

  • Обязательные: fun add(a: Int, b: Int)
  • Параметры по умолчанию: fun log(message: String, level: String = "INFO")
  • Именованные аргументы: log("Error", level = "ERROR")

Порядок аргументов может быть изменён при использовании именованных параметров.

Единичный тип

Функция без возвращаемого значения имеет тип Unit:

fun printHello(): Unit {
println("Hello")
}
// Эквивалентно:
fun printHello() {
println("Hello")
}

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

Функции могут принимать другие функции в качестве параметров:

fun operate(a: Int, b: Int, operation: (Int, Int) -> Int): Int {
return operation(a, b)
}

val result = operate(5, 3) { x, y -> x + y }

Лямбда-выражения

Синтаксис: { параметры -> тело }

val sum = { x: Int, y: Int -> x + y }
val isEmpty = { s: String -> s.isEmpty() }

Если лямбда — последний аргумент, её можно вынести за скобки:

list.filter { it > 0 }.map { it * 2 }

Ключевое слово it — неявный параметр единственного аргумента.

Inline-функции

Функции, помеченные inline, встраиваются в точку вызова, устраняя накладные расходы:

inline fun measureTime(block: () -> Unit): Long {
val start = System.currentTimeMillis()
block()
return System.currentTimeMillis() - start
}

Расширения

Функции и свойства могут быть добавлены к существующим классам без наследования:

fun String.isValidEmail(): Boolean {
return this.contains("@")
}

val email = "user@example.com"
println(email.isValidEmail()) // true

Расширения не модифицируют исходный класс. Они работают статически.

Инфиксные функции

Функции, помеченные infix, вызываются без скобок и точки:

infix fun Int.times(str: String) = str.repeat(this)

val result = 3 times "Kotlin " // "Kotlin Kotlin Kotlin "

Требования: функция должна быть членом класса или расширением, принимать один параметр, не использовать параметры по умолчанию.

Операторные функции

Перегрузка операторов через специальные имена:

operator fun Point.plus(other: Point) = Point(x + other.x, y + other.y)

Поддерживаемые операторы: +, -, *, /, %, ==, !=, >, <, >=, <=, [], in, .., unaryPlus, inc, dec, invoke и другие.

Область видимости и модификаторы доступа

Kotlin предоставляет следующие модификаторы:

  • public — видимо везде (по умолчанию)
  • private — только внутри файла (для топ-левел) или класса
  • protected — внутри класса и его подклассов
  • internal — внутри модуля (все файлы, собранные вместе)

Пример:

internal class InternalService {
private fun secret() { }
protected fun forSubclasses() { }
public fun api() { }
}

Исключения

Kotlin использует механизм исключений, аналогичный Java, но без проверяемых исключений:

throw IllegalArgumentException("Invalid argument")

Обработка:

try {
riskyOperation()
} catch (e: IOException) {
handleIoError(e)
} finally {
cleanup()
}

Исключения — unchecked. Функции не обязаны декларировать выбрасываемые исключения.


Классы, объекты и продвинутые типы

Классы

Класс в Kotlin объявляется ключевым словом class. Минимальное объявление:

class Person

Класс может содержать:

  • Первичный конструктор (в заголовке класса)
  • Вторичные конструкторы (constructor)
  • Свойства (val/var)
  • Функции
  • Инициализирующие блоки (init)
  • Вложенные и внутренние классы

Первичный конструктор

Первичный конструктор указывается прямо в заголовке класса:

class Person(val name: String, var age: Int)

Параметры с val или var автоматически становятся свойствами экземпляра.

Если требуется логика инициализации, используется блок init:

class Person(name: String, age: Int) {
val formattedName = name.capitalize()

init {
require(age >= 0) { "Age must be non-negative" }
}
}

Первичный конструктор может иметь аннотации и модификаторы видимости:

class Person internal constructor(val name: String)

Вторичные конструкторы

Вторичные конструкторы объявляются с помощью ключевого слова constructor и должны делегировать вызов первичному конструктору или другому вторичному:

class Person(val name: String) {
constructor(name: String, age: Int) : this(name) {
// дополнительная логика
}
}

Если у класса нет первичного конструктора, вторичный может вызывать суперкласс напрямую.

Наследование

По умолчанию классы в Kotlin запечатаны (final). Чтобы разрешить наследование, класс должен быть помечен как open:

open class Animal(val name: String)

class Dog(name: String) : Animal(name)

Конструктор суперкласса вызывается при объявлении подкласса.

Переопределение методов и свойств требует явного указания override:

open class Animal {
open fun makeSound() = "..."
}

class Dog : Animal() {
override fun makeSound() = "Woof!"
}

Свойства также могут быть переопределены:

open class Rectangle(open val width: Int, open val height: Int)

class Square(override val width: Int) : Rectangle(width, width) {
override val height: Int get() = width
}

Абстрактные классы

Абстрактный класс объявляется с модификатором abstract. Он не может быть инстанцирован и может содержать абстрактные методы и свойства:

abstract class Shape {
abstract val area: Double
abstract fun draw()
}

Подклассы обязаны реализовать все абстрактные члены.

Интерфейсы

Интерфейсы объявляются с помощью interface. Они могут содержать:

  • Абстрактные методы
  • Методы с реализацией по умолчанию
  • Абстрактные свойства
  • Свойства с реализацией через геттер
interface Drawable {
val color: String
fun draw()
fun resize(factor: Double) {
println("Resizing by $factor")
}
}

Класс может реализовать несколько интерфейсов:

class Circle(override val color: String) : Drawable {
override fun draw() {
println("Drawing circle in $color")
}
}

Разрешение конфликтов при множественном наследовании реализации:

class Hybrid : InterfaceA, InterfaceB {
override fun method() {
super<InterfaceA>.method()
}
}

Объекты

Kotlin предоставляет встроенные механизмы для создания одиночек (singleton):

Object declaration

object Logger {
fun log(message: String) {
println("[LOG] $message")
}
}

Обращение: Logger.log("Hello")

Такой объект создаётся при первом обращении к нему (ленивая инициализация, потокобезопасная).

Компаньон-объекты

Компаньон-объект — это объект, связанный с классом. Его члены доступны через имя класса, как статические члены в Java:

class StringUtils {
companion object {
fun isEmpty(s: String) = s.isEmpty()
}
}

// Вызов:
StringUtils.isEmpty("test")

Компаньон-объект может иметь имя:

companion object Factory {
fun create() = MyClass()
}

Компаньон-объект наследует интерфейсы и может содержать расширения.

Object expressions

Анонимные объекты создаются с помощью object:

val listener = object : OnClickListener {
override fun onClick() {
println("Clicked!")
}
}

Если объект наследует только Any, скобки после object опускаются:

val obj = object {
val x = 10
fun greet() = "Hello"
}

Data-классы

Data-классы предназначены для хранения данных. Они автоматически получают реализации:

  • equals()
  • hashCode()
  • toString()
  • copy()
  • Компонентные функции (component1(), component2(), …)

Объявление:

data class User(val id: Int, val name: String, val email: String)

Ограничения:

  • Должен иметь хотя бы один параметр в первичном конструкторе
  • Все параметры конструктора должны быть val или var
  • Не может быть abstract, open, sealed, inner

Метод copy() позволяет создавать изменённые копии:

val user = User(1, "Alice", "alice@example.com")
val updated = user.copy(email = "new@example.com")

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

val (id, name) = user

Sealed-классы

Sealed-классы представляют ограниченную иерархию типов. Все подклассы должны быть объявлены в том же файле:

sealed class Result
data class Success(val data: String) : Result()
data class Error(val message: String) : Result()
object Loading : Result()

Используются в when для исчерпывающей проверки:

fun handle(result: Result) = when (result) {
is Success -> println(result.data)
is Error -> println(result.message)
Loading -> println("Loading...")
}

Компилятор гарантирует, что все возможные подтипы учтены.

Value-классы (Kotlin 1.5+)

Value-классы — легковесные обёртки без накладных расходов на рантайме:

@JvmInline
value class UserId(val value: Long)

Ограничения:

  • Ровно одно свойство val в первичном конструкторе
  • Не может иметь init-блоков
  • Не может наследовать другие классы
  • Не может быть наследуемым

На JVM компилируется в примитивный тип, когда это возможно.

Inline-классы (устаревшее, заменены value-классами)

Ранее использовались те же цели, но теперь рекомендуется использовать value class.

Перечисления (enum)

Перечисления объявляются с помощью enum class:

enum class Color {
RED, GREEN, BLUE
}

Могут содержать свойства и методы:

enum class HttpStatus(val code: Int) {
OK(200),
NOT_FOUND(404),
SERVER_ERROR(500);

fun isSuccess() = code in 200..299
}

Доступ к константам: HttpStatus.OK

Методы: values(), valueOf("OK")

Делегирование

Kotlin поддерживает делегирование на уровне языка:

Классовое делегирование

interface Base {
fun print()
}

class BaseImpl(val x: Int) : Base {
override fun print() = println(x)
}

class Derived(b: Base) : Base by b

Вызов Derived(BaseImpl(10)).print() делегируется BaseImpl.

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

Синтаксис: val/var <property> by <delegate>

Встроенные делегаты:

  • lazy — ленивая инициализация
  • observable — отслеживание изменений
  • vetoable — возможность отменить изменение
  • notNull — отложенная инициализация без null
  • map — хранение в Map

Пример lazy:

val lazyValue: String by lazy {
println("Computed!")
"Hello"
}

Вычисление происходит при первом обращении.

Пример observable:

var name: String by Delegates.observable("Alice") { prop, old, new ->
println("${prop.name}: $old$new")
}

Вложенные и внутренние классы

  • Вложенный класс (nested) — статический по смыслу, не имеет доступа к внешнему экземпляру
  • Внутренний класс (inner) — имеет доступ к экземпляру внешнего класса
class Outer {
private val bar: Int = 1

class Nested {
fun foo() = 2
}

inner class Inner {
fun foo() = bar // доступ к bar
}
}

val nested = Outer.Nested().foo() // 2
val inner = Outer().Inner().foo() // 1

Локальные классы

Классы могут быть объявлены внутри функций:

fun createCounter(): () -> Int {
class Counter(var count: Int = 0) {
fun next() = ++count
}
val counter = Counter()
return counter::next
}

Локальный класс имеет доступ к параметрам и переменным функции.

Аннотации

Аннотации в Kotlin похожи на Java, но с расширенными возможностями:

annotation class JsonName(val name: String)

data class User(
@JsonName("user_id") val id: Int,
val name: String
)

Целевые аннотации:

@get:JsonName("full_name")
val fullName: String

Возможные цели: file, property, field, get, set, receiver, param, setparam, delegate


Коллекции, последовательности, строки и диапазоны

Коллекции

Kotlin предоставляет богатую иерархию коллекций, полностью совместимую с Java, но с дополнительными функциональными возможностями. Все коллекции неизменяемы по умолчанию. Изменяемые версии доступны отдельно.

Иерархия коллекций

  • Collection<T> — базовый интерфейс для всех коллекций
    • List<T> — упорядоченная коллекция с доступом по индексу
      • MutableList<T>
    • Set<T> — коллекция без дубликатов
      • MutableSet<T>
    • Map<K, V> — ассоциативный массив (ключ-значение)
      • MutableMap<K, V>

Создание коллекций

Списки:

val empty = emptyList<String>()
val list = listOf("a", "b", "c")
val mutable = mutableListOf("x", "y")
val arrayList = arrayListOf(1, 2, 3)

Множества:

val set = setOf(1, 2, 3)
val mutableSet = mutableSetOf("apple", "banana")
val hashSet = hashSetOf(10, 20)

Словари:

val map = mapOf("name" to "Alice", "age" to 30)
val mutableMap = mutableMapOf("key" to "value")
val hashMap = hashMapOf(1 to "one", 2 to "two")

Оператор to создаёт экземпляр Pair.

Основные свойства

  • size — количество элементов
  • isEmpty(), isNotEmpty() — проверка на пустоту
  • first(), last() — первый и последний элемент
  • firstOrNull(), lastOrNull() — безопасные аналоги
  • elementAt(index), elementAtOrNull(index) — доступ по индексу

Проверки содержимого

list.contains("a")      // true/false
"a" in list // синтаксический сахар
list.any { it.length > 3 }
list.all { it.isNotBlank() }
list.none { it.isEmpty() }

Функциональные операции над коллекциями

Kotlin предлагает обширный набор функций высшего порядка для трансформации и фильтрации.

Трансформация

  • map { ... } — преобразует каждый элемент
  • mapIndexed { index, value -> ... } — с доступом к индексу
  • flatMap { ... } — применяет функцию, возвращающую коллекцию, и выравнивает результат
  • mapNotNull { ... } — как map, но отбрасывает null

Пример:

val names = users.map { it.name }
val lengths = words.map { it.length }
val allChars = words.flatMap { it.toList() }

Фильтрация

  • filter { ... } — оставляет элементы, удовлетворяющие условию
  • filterNot { ... } — оставляет элементы, не удовлетворяющие условию
  • filterIndexed { index, value -> ... }
  • filterIsInstance<T>() — оставляет только экземпляры типа T
  • take(n) — первые n элементов
  • takeWhile { ... } — берёт элементы, пока условие истинно
  • drop(n) — пропускает первые n
  • dropWhile { ... } — пропускает, пока условие истинно

Пример:

val adults = people.filter { it.age >= 18 }
val numbers = list.filterIsInstance<Int>()

Группировка и разбиение

  • groupBy { keySelector } — группирует по ключу
  • groupingBy { ... } — ленивая группировка (для цепочек)
  • partition { predicate } — разделяет на две части: соответствующие и не соответствующие предикату

Пример:

val byLength = words.groupBy { it.length }
val (valid, invalid) = emails.partition { isValid(it) }

Агрегация и свёртка

  • fold(initial) { acc, value -> ... } — накопление слева направо
  • reduce { acc, value -> ... } — как fold, но без начального значения (использует первый элемент)
  • sum(), sumOf { ... }
  • minOrNull(), maxOrNull()
  • minByOrNull { ... }, maxByOrNull { ... }
  • average(), count()

Пример:

val total = prices.sum()
val longest = words.maxByOrNull { it.length }
val product = numbers.fold(1) { acc, n -> acc * n }

Объединение и сравнение

  • union(other) — объединение без дубликатов (+ для Set)
  • intersect(other) — пересечение
  • subtract(other) — разность
  • zip(other) — объединяет два списка в список пар
  • unzip() — обратная операция для zip

Пример:

val pairs = names.zip(ages) // List<Pair<String, Int>>
val (names2, ages2) = pairs.unzip()

Сортировка

  • sorted() — по возрастанию (для Comparable)
  • sortedBy { selector } — по значению селектора
  • sortedDescending()
  • sortedByDescending { ... }
  • reversed() — в обратном порядке

Пример:

val sorted = users.sortedBy { it.lastName }

Последовательности (Sequence)

Последовательности — ленивые коллекции. Операции выполняются поэлементно и только при необходимости.

Создание:

val seq = sequenceOf(1, 2, 3)
val fromIterable = list.asSequence()
val generate = generateSequence(1) { it + 1 } // бесконечная последовательность

Преимущество: эффективность при цепочках операций, особенно с filter и map.

Завершение последовательности:

val result = seq
.filter { it % 2 == 0 }
.map { it * it }
.take(5)
.toList() // терминальная операция

Без .toList() или другой терминальной операции (first(), count(), forEach) вычисления не происходят.

Диапазоны и прогрессии

Диапазоны представляют собой интервалы значений.

Числовые диапазоны

val r1 = 1..5        // [1, 5] включительно
val r2 = 1 until 5 // [1, 4]
val r3 = 5 downTo 1 // [5, 1]
val r4 = 1..10 step 2 // 1, 3, 5, 7, 9

Проверка принадлежности:

if (x in 1..10) { ... }
if (c !in 'a'..'z') { ... } // допустимо для Char

Диапазоны символов

val letters = 'a'..'z'
val hexDigits = '0'..'9' + 'A'..'F'

Прогрессии

Интерфейс Progression<T> лежит в основе всех диапазонов. Реализации: IntProgression, LongProgression, CharProgression.

Методы: first, last, step, iterator()

Работа со строками

Строки в Kotlin неизменяемы и основаны на java.lang.String.

Основные методы

  • length — длина строки
  • isEmpty(), isBlank() — пустая или только пробельные символы
  • trim(), trimStart(), trimEnd()
  • toUpperCase(), toLowerCase() (лучше использовать uppercase(), lowercase() в новых версиях)
  • substring(start, end)
  • replace(old, new), replace(regex, replacement)
  • split(delimiter)

Проверки

str.startsWith("prefix")
str.endsWith("suffix")
str.contains("text")
str.matches(Regex("pattern"))

Регулярные выражения

Создание:

val regex = Regex("[a-z]+")
val pattern = "[0-9]+".toRegex()

Методы:

  • matches(input) — вся строка должна соответствовать
  • containsMatchIn(input)
  • find(input) — возвращает MatchResult?
  • findAll(input) — последовательность совпадений
  • replace(input, replacement)

Пример:

val emailRegex = Regex("^[\\w.-]+@[\\w.-]+\\.[a-zA-Z]{2,}\$")
val isValid = emailRegex.matches(email)

Преобразования

  • toInt(), toDouble(), toBoolean() — с выбросом исключения при ошибке
  • toIntOrNull(), toDoubleOrNull() — безопасные аналоги
  • joinToString() — объединение коллекции в строку

Пример:

val nums = listOf(1, 2, 3)
val str = nums.joinToString(prefix = "[", postfix = "]", separator = ", ")
// "[1, 2, 3]"

Расширенные возможности строк

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

Уже упоминались ранее:

val sql = """
SELECT *
FROM users
WHERE active = true
""".trimIndent()

Raw-строки

В тройных кавычках обратный слеш не экранируется:

val path = """C:\Users\Name\Documents"""

Расширения для строк

Часто используемые расширения:

fun String?.orEmpty(): String = this ?: ""
fun String.lines(): List<String>
fun String.capitalize(): String // устарело, лучше использовать replaceFirstChar

Корутины, асинхронность, обработка ошибок и DSL

Корутины

Корутины — это легковесные потоки выполнения, управляемые библиотекой kotlinx.coroutines. Они позволяют писать асинхронный код в императивном стиле без коллбэков.

Основные понятия

  • Корутина — экземпляр асинхронного вычисления.
  • Контекст (CoroutineContext) — окружение корутины: диспетчер, задание, исключения, имя.
  • Диспетчер (CoroutineDispatcher) — определяет, в каком потоке или пуле потоков выполняется корутина.
  • Задание (Job) — иерархический жизненный цикл корутины, поддерживает отмену.
  • Область (CoroutineScope) — привязывает корутины к определённому жизненному циклу.

Запуск корутин

Три основных способа:

  1. launch — запускает корутину «в фоне», не возвращает результат напрямую.
  2. async — запускает корутину и возвращает Deferred<T>, из которого можно получить результат через await().
  3. runBlocking — блокирует текущий поток до завершения корутины (используется в основном в тестах и main).

Пример:

import kotlinx.coroutines.*

fun main() = runBlocking {
val job = launch {
delay(1000)
println("World!")
}
println("Hello,")
job.join()
}

Диспетчеры

  • Dispatchers.Default — для CPU-интенсивных задач (пул общего назначения).
  • Dispatchers.IO — для операций ввода-вывода (файлы, сеть, БД).
  • Dispatchers.Main — главный поток (Android UI, JavaFX, Swing).
  • Dispatchers.Unconfined — запускает корутину в текущем потоке, но продолжает в том, где произошла приостановка (риск!).
  • Пользовательские диспетчеры: newSingleThreadContext, newFixedThreadPoolContext.

Пример переключения контекста:

withContext(Dispatchers.IO) {
// чтение файла
}

Отмена и таймауты

Корутины поддерживают кооперативную отмену:

val job = launch {
repeat(1000) { i ->
if (!isActive) return@launch
println("Iteration $i")
delay(100)
}
}
delay(500)
job.cancel()

Или с использованием ensureActive().

Таймаут:

withTimeout(1000) {
// долгая операция
}
// выбрасывает CancellationException при превышении

Безопасный таймаут с возвратом значения:

val result = withTimeoutOrNull(1000) {
fetchData()
} ?: "Default"

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

Исключения в корутинах распространяются по иерархии Job.

  • В launch — исключение должно быть обработано явно, иначе оно «утекает».
  • В async — исключение сохраняется в Deferred и выбрасывается при вызове await().

Способы обработки:

  1. try/catch внутри корутины
  2. CoroutineExceptionHandler
val handler = CoroutineExceptionHandler { _, exception ->
println("Caught: $exception")
}

val job = GlobalScope.launch(handler) {
throw RuntimeException("Oops!")
}
  1. Supervision — использование SupervisorJob или supervisorScope, чтобы сбой одного ребёнка не отменял других.
supervisorScope {
val child1 = launch { ... }
val child2 = launch { throw Error() }
// child1 продолжит работу
}

Тип Result

Result<T> — встроенный тип для безопасной обработки операций, которые могут завершиться ошибкой.

Создание:

val success = Result.success("OK")
val failure = Result.failure<String>(IOException("Failed"))

Использование:

fun process(): Result<String> = runCatching {
riskyOperation()
}

val result = process()
when {
result.isSuccess -> println(result.getOrNull())
result.isFailure -> println(result.exceptionOrNull())
}

Методы:

  • map { ... }
  • mapCatching { ... }
  • getOrElse { ... }
  • getOrThrow() — выбрасывает исключение при ошибке

Ограничение: Result нельзя использовать как тип параметра функции или свойства (только в локальных переменных и возвращаемых значениях).

DSL (Domain-Specific Languages)

Kotlin предоставляет мощные средства для создания внутренних DSL:

Приёмники (receiver)

Функции расширения и лямбды с приёмником:

html {
body {
h1 { +"Hello" }
p { +"World" }
}
}

Реализация:

class HTML {
fun body(init: BODY.() -> Unit): BODY {
val body = BODY()
body.init()
children.add(body)
return body
}
}

fun html(init: HTML.() -> Unit): HTML {
val html = HTML()
html.init()
return html
}

Лямбда с приёмником

Тип: T.() -> R — внутри лямбды this имеет тип T.

apply, with, run, let, also — стандартные DSL-утилиты

  • apply: T.apply(block: T.() -> Unit): T — инициализация объекта
  • with: with(obj, block: T.() -> R): R — группировка операций над объектом
  • run: T.run(block: T.() -> R): R — как with, но вызывается как метод
  • let: T.let(block: (T) -> R): R — безопасная работа с nullable, преобразование
  • also: T.also(block: (T) -> Unit): T — побочные эффекты (логирование, отладка)

Пример:

val person = Person().apply {
name = "Alice"
age = 30
}

Аннотации и рефлексия

Встроенные аннотации

  • @Deprecated — помечает устаревший API
  • @Suppress — подавляет предупреждения компилятора
  • @JvmStatic, @JvmField, @JvmOverloads — совместимость с Java
  • @Throws — указывает возможные исключения (для Java-совместимости)
  • @PublishedApi — делает internal-член видимым для inline-функций вне модуля

Рефлексия

Требует зависимости kotlin-reflect.

Получение KClass:

val clazz = MyClass::class
val obj = clazz.createInstance() // если есть конструктор без параметров

Инспекция свойств и функций:

for (member in clazz.members) {
println(member.name)
}

Получение значения свойства:

val prop = clazz.memberProperties.find { it.name == "name" }
val value = prop?.get(instance)

Multiplatform и expect/actual

Kotlin поддерживает кроссплатформенную разработку.

Объявление общего кода:

expect fun platformName(): String

Реализация для JVM:

actual fun platformName(): String = "JVM"

Реализация для JS:

actual fun platformName(): String = "JS"

Используется в commonMain, jvmMain, jsMain и других source sets.

Настройки проекта (build.gradle.kts)

Типичная конфигурация для Kotlin/JVM:

plugins {
kotlin("jvm") version "2.0.0"
}

repositories {
mavenCentral()
}

dependencies {
implementation(kotlin("stdlib"))
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0")
testImplementation(kotlin("test"))
}

tasks.test {
useJUnitPlatform()
}

kotlin {
jvmToolchain(17)
}

Для Multiplatform:

kotlin {
jvm()
js(IR) { browser() }
sourceSets {
commonMain.dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0")
}
}
}

Полезные библиотеки из kotlinx

  • kotlinx-coroutines-core — корутины
  • kotlinx-datetime — работа с датой и временем
  • kotlinx-serialization — сериализация JSON, Protobuf и др.
  • kotlinx-html — DSL для генерации HTML
  • kotlinx-cli — парсинг аргументов командной строки

Пример сериализации:

@Serializable
data class User(val name: String, val age: Int)

val json = Json.encodeToString(User("Alice", 30))
val user = Json.decodeFromString<User>(json)

Ввод-вывод, взаимодействие с Java, идиомы, инструменты и лучшие практики

Работа с файлами и вводом-выводом

Kotlin использует стандартные классы Java для работы с файловой системой, но предоставляет удобные расширения.

Чтение и запись файлов

Чтение всего файла:

val text = File("data.txt").readText()
val lines = File("data.txt").readLines()

Запись:

File("output.txt").writeText("Hello")
File("log.txt").appendText("\nNew entry")

Безопасная работа через use (аналог try-with-resources):

File("data.txt").bufferedReader().use { reader ->
reader.forEachLine { line ->
println(line)
}
}

Создание директорий:

val dir = File("mydir")
dir.mkdirs() // создаёт всю цепочку директорий при необходимости

Проверки:

file.exists()
file.isDirectory
file.isFile
file.canRead()
file.length()

Потоки и ресурсы

Расширение use применяется к любому объекту, реализующему Closeable:

InputStreamReader(inputStream).use { reader ->
// работа с reader
}

Это гарантирует вызов close() даже при исключении.

Взаимодействие с Java

Kotlin полностью совместим с Java. Однако есть нюансы.

Вызов Kotlin из Java

  • package и имя файла не влияют на имя класса.
  • Top-level функции компилируются в статические методы в классе с суффиксом Kt:
    // utils.kt
    fun formatDate(date: Date): String = ...
    В Java: UtilsKt.formatDate(date);
  • Чтобы задать имя класса, используйте @file:JvmName("Utils") в начале файла.
  • @JvmOverloads генерирует перегрузки для параметров по умолчанию.
  • @JvmStatic делает метод или свойство компаньона статическим в Java.
  • @JvmField превращает свойство в публичное поле без геттера/сеттера.

Вызов Java из Kotlin

  • Все ссылки из Java считаются платформенными типами (String!), то есть nullable и non-nullable одновременно.
  • Kotlin не видит аннотации @Nullable/@NonNull по умолчанию, но поддерживает:
    • @Nullable, @NotNull (JetBrains)
    • @Nullable, @NonNull (Android)
    • JSR-305 (javax.annotation)
  • Массивы Java отображаются как Array<T> или примитивные массивы.
  • Checked-исключения Java игнорируются в Kotlin.

Обработка статических членов и фабрик

Java-фабрики:

// Java
public class Box {
public static Box create(String content) { ... }
}

В Kotlin:

val box = Box.create("content")

Если в Java используется паттерн Builder, его можно обернуть в DSL:

fun box(init: Box.Builder.() -> Unit): Box {
return Box.Builder().apply(init).build()
}

// Использование:
val myBox = box {
setContent("Hello")
setColor("red")
}

Идиомы и паттерны Kotlin

Безопасное преобразование типов

if (obj is String) {
println(obj.length) // obj автоматически приведён к String
}

Или с элвисом:

val length = (obj as? String)?.length ?: 0

Цепочка вызовов с обработкой null

val result = service
?.getUser(id)
?.address
?.city
?.uppercase()
?: "Unknown"

Использование sealed class вместо enum с данными

sealed interface NetworkResult
data class Success(val data: List<Item>) : NetworkResult
data class Failure(val error: Throwable) : NetworkResult
object Loading : NetworkResult

Полный контроль в when.

Делегирование вместо наследования

class ObservableList<T>(
private val delegate: MutableList<T>
) : MutableList<T> by delegate {
private val observers = mutableListOf<(List<T>) -> Unit>()

override fun add(element: T): Boolean {
val result = delegate.add(element)
notifyObservers()
return result
}

fun addObserver(observer: (List<T>) -> Unit) {
observers += observer
}

private fun notifyObservers() {
observers.forEach { it(delegate) }
}
}

Однократная инициализация

val config by lazy { loadConfig() }

Конструкторы с именованными параметрами как замена билдерам

val person = Person(
name = "Alice",
age = 30,
email = "alice@example.com",
isActive = true
)

Устраняет необходимость в паттерне Builder для большинства случаев.

Производительность и best practices

Избегайте boxed-типов там, где возможны примитивы

  • Используйте IntArray, DoubleArray, а не Array<Int>, если не нужна nullable-семантика.
  • В коллекциях примитивы всегда оборачиваются, но в массивах — нет.

Осторожно с корутинами в циклах

Неправильно:

for (item in items) {
launch { process(item) } // создаёт тысячи корутин
}

Правильно:

coroutineScope {
items.map { item ->
async { process(item) }
}.awaitAll()
}

Или ограничение параллелизма:

items.chunked(10).forEach { chunk ->
coroutineScope {
chunk.map { async { process(it) } }.awaitAll()
}
}

Не используйте GlobalScope

Вместо этого создавайте явные CoroutineScope с привязкой к жизненному циклу:

class MyService {
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)

fun start() {
scope.launch { ... }
}

fun shutdown() {
scope.cancel()
}
}

Избегайте !! без крайней необходимости

Предпочтительны:

  • ?.
  • ?:
  • let
  • Явная проверка if (x != null)

Инструменты разработки

KDoc

Документация в стиле Kotlin:

/**
* Calculates the factorial of [n].
*
* @param n a non-negative integer
* @return the factorial of [n]
* @throws IllegalArgumentException if [n] is negative
*/
fun factorial(n: Int): Long { ... }

Генерируется через Dokka.

ktlint

Форматирование кода по официальным правилам Kotlin:

ktlint "src/**/*.kt"
ktlint -F "src/**/*.kt" # автоисправление

Можно интегрировать в Gradle.

detekt

Статический анализатор кода:

# detekt.yml
style:
MagicNumber:
active: true
ReturnCount:
max: 3

Обнаруживает code smells, дублирование, сложность.

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

  • kotlin.test — кроссплатформенный фреймворк
  • JUnit 5 — стандарт для JVM
  • Kotest — мощный альтернативный фреймворк с DSL

Пример:

@Test
fun `user name should not be empty`() {
val user = User("Alice")
assertTrue(user.name.isNotEmpty())
}

Советы по миграции с Java

  1. Начните с data-классов — они заменяют POJO с геттерами, сеттерами, equals, hashCode.
  2. Замените Optional на nullable-типыString? вместо Optional<String>.
  3. Используйте when вместо switch — мощнее и безопаснее.
  4. Переведите коллекции на функциональные операцииfilter, map, find.
  5. Замените билдеры на именованные параметры и apply.
  6. Переведите коллбэки на корутины или suspend-функции.
  7. Используйте sealed class для замены enum с состояниями и данными.