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 типы
Числовые типы
| Тип | Размер (бит) | Диапазон значений |
|---|---|---|
Byte | 8 | -128 до 127 |
Short | 16 | -32768 до 32767 |
Int | 32 | -2³¹ до 2³¹−1 |
Long | 64 | -2⁶³ до 2⁶³−1 |
Float | 32 | IEEE 754 |
Double | 64 | IEEE 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— не может бытьnullString?— может быть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— отложенная инициализация без nullmap— хранение в 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>()— оставляет только экземпляры типаTtake(n)— первыеnэлементовtakeWhile { ... }— берёт элементы, пока условие истинноdrop(n)— пропускает первыеndropWhile { ... }— пропускает, пока условие истинно
Пример:
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) — привязывает корутины к определённому жизненному циклу.
Запуск корутин
Три основных способа:
launch— запускает корутину «в фоне», не возвращает результат напрямую.async— запускает корутину и возвращаетDeferred<T>, из которого можно получить результат черезawait().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().
Способы обработки:
try/catchвнутри корутиныCoroutineExceptionHandler
val handler = CoroutineExceptionHandler { _, exception ->
println("Caught: $exception")
}
val job = GlobalScope.launch(handler) {
throw RuntimeException("Oops!")
}
- 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 для генерации HTMLkotlinx-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:В Java:// utils.kt
fun formatDate(date: Date): String = ...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
- Начните с data-классов — они заменяют POJO с геттерами, сеттерами,
equals,hashCode. - Замените
Optionalна nullable-типы —String?вместоOptional<String>. - Используйте
whenвместоswitch— мощнее и безопаснее. - Переведите коллекции на функциональные операции —
filter,map,find. - Замените билдеры на именованные параметры и
apply. - Переведите коллбэки на корутины или
suspend-функции. - Используйте
sealed classдля замены enum с состояниями и данными.