Kotlin — современный язык от JetBrains, работающий на JVM (а также на Android, в браузере через Kotlin/JS, нативно через Kotlin/Native). Полностью совместим с Java.
Главные отличия: null-безопасность на уровне типов, data class, extension functions, корутины, лаконичный синтаксис, smart casts, scope-функции (let, run, also, apply, with), поддержка функционального стиля.
Из практики: на Android Kotlin стал основным языком, для бэкенда — частый выбор для новых сервисов. Совместимость с Java позволяет постепенно мигрировать или смешивать в одном проекте.
Частые ошибки
Писать на Kotlin в Java-стиле, не используя его сильные стороны.
Игнорировать null-безопасность и расставлять !! везде — это лишает её смысла.
Дополнительные вопросы
Можно ли использовать Java-библиотеки из Kotlin?
Чем data class отличается от обычного класса?
Чем val отличается от var в Kotlin?
Junior
val — это неизменяемая ссылка (read-only), её нельзя переприсвоить после инициализации. var — изменяемая, можно переприсваивать.
Важно: val не делает объект иммутабельным, только ссылку. val list = mutableListOf(1, 2) — нельзя переприсвоить list, но можно list.add(3).
Из практики: по умолчанию всегда используйте val. Кода становится предсказуемее, проще читать и тестировать. var включают, только когда без него не обойтись.
Частые ошибки
Думать, что val значит «иммутабельный объект» — это лишь иммутабельная ссылка.
Использовать var по привычке из Java.
Дополнительные вопросы
Чем val отличается от const val?
Можно ли val переопределить в наследнике?
Как работает null-безопасность в Kotlin?
Junior
Тип в Kotlin по умолчанию ненулевой: String — нельзя присвоить null. Для допустимого null используют тип с вопросом: String?. Компилятор требует обрабатывать null-случай.
Операторы: ?. (безопасный вызов, вернёт null если получатель null), ?: (Elvis — значение по умолчанию, если null), !! (утверждение «точно не null», бросит NPE если ошиблись).
Из практики: правильно расставленные ?, ?., ?: и отказ от !! почти полностью устраняют NullPointerException. Это одна из основных причин выбора Kotlin.
Частые ошибки
Использовать !! везде — теряется смысл null-safety.
Получать данные из Java и забывать про platform-типы (тип без гарантий).
Дополнительные вопросы
Что такое platform type?
Как работает оператор Elvis?
Что такое data class и зачем он нужен?
Junior
data class — это класс, у которого компилятор автоматически генерирует equals, hashCode, toString, copy и componentN для деструктуризации. Объявляется ключевым словом data перед class.
Подходит для классов-носителей данных: модели API, DTO, value-объекты. data class User(val name: String, val age: Int).
Из практики: copy позволяет создавать слегка изменённую копию, не мутируя оригинал: user.copy(age = 31). Это поддерживает иммутабельный стиль.
Частые ошибки
Использовать data class для бизнес-логики — он не подходит для сущностей с identity.
Сравнивать data class по структуре, забыв, что equals учитывает все объявленные в конструкторе свойства.
Дополнительные вопросы
Можно ли наследовать data class?
Что такое componentN и как работает деструктуризация?
Что такое extension function в Kotlin?
Middle
Расширяющая функция позволяет добавить к существующему типу новый метод без наследования и модификации исходного класса. fun String.lastChar(): Char = this[this.length - 1] — теперь любая String умеет lastChar().
Под капотом это обычная статическая функция, принимающая получатель первым аргументом. Поэтому диспатч статический, без виртуальности.
Из практики: extension functions помогают создавать читаемые DSL, расширять чужие классы (включая стандартную библиотеку) без обёрток и держать вспомогательную логику рядом с типом.
Частые ошибки
Считать, что extension переопределяет метод — нет, при конфликте побеждает обычный метод класса.
Расширять часто используемые типы (String, List) и засорять глобальную область.
Дополнительные вопросы
Можно ли сделать extension property?
Как работает разрешение конфликтов между extension и членом класса?
Что такое scope-функции let, run, also, apply, with?
Middle
Это пять функций, которые принимают лямбду и работают со «своим» объектом. Различаются тем, что внутри (this или it) и что возвращают (получатель или результат лямбды).
let — it, возвращает результат лямбды (часто для null?.let). run — this, возвращает результат лямбды (для конфигурации с возвратом). also — it, возвращает получатель (для побочных эффектов). apply — this, возвращает получатель (для инициализации). with — не extension, this, возвращает результат лямбды.
Из практики: typical idioms — value?.let { ... } для безопасной работы с null, builder().apply { fieldA = 1 } для конструирования, save().also { log.info(it) } для логирования.
Частые ошибки
Злоупотреблять scope-функциями и делать код нечитаемым.
Перепутать it и this и потерять понимание, что куда передаётся.
Дополнительные вопросы
В каком сценарии выбрать let, а в каком apply?
Что вернёт val r = obj.also { ... }?
Что такое корутины (coroutines) и зачем они нужны?
Middle
Корутина — это лёгкий поток исполнения, который может приостанавливаться (suspend) и возобновляться без блокировки треда. На одном тысячах корутин запросто работают на десятке OS-потоков.
Запускают через билдеры: launch (fire-and-forget), async (с результатом, возвращает Deferred), runBlocking (для тестов и точек входа). suspend-функции — это функции, которые умеют приостанавливаться.
Из практики: для асинхронной работы (сетевые запросы, БД, чтение файлов) корутины дают код, читающийся как синхронный, но без блокировки потоков. Решают те же задачи, что и потоки/RxJava, но проще.
Частые ошибки
Запускать корутину в GlobalScope в продакшене — потеря структурного параллелизма, утечки.
Звать runBlocking в коде на горячем пути — заблокирует поток.
Дополнительные вопросы
Чем launch отличается от async?
Что такое структурный параллелизм?
Что делает ключевое слово suspend?
Middle
suspend помечает функцию, которая может приостанавливаться. Её можно вызывать только из другой suspend-функции или внутри корутины (launch/async/runBlocking).
Компилятор превращает suspend-функцию в state machine, передавая Continuation. Снаружи это похоже на обычный вызов, но под капотом — машина состояний.
Из практики: suspend-функции — это асинхронный код, который читается как синхронный. Подходит для сетевых запросов, БД, файловых операций. Передавайте все асинхронные API через suspend, не маскируйте runBlocking.
Частые ошибки
Делать тяжёлые CPU-вычисления в suspend без withContext(Dispatchers.Default).
Маркировать suspend функцию, не использующую suspension — это «загрязняет» сигнатуру без пользы.
Дополнительные вопросы
Что такое Continuation?
Как делать blocking IO в suspend-функции правильно?
Чем launch отличается от async в корутинах?
Middle
launch запускает корутину и возвращает Job — управляющий объект без значения результата. Используется для побочных действий (логирование, отправка событий).
async запускает и возвращает Deferred<T> — это Job, у которой есть await(), возвращающий результат. Используется для параллельных вычислений с результатом.
Из практики: для двух параллельных запросов делают val a = async { fetchA() }; val b = async { fetchB() }; val res = a.await() + b.await().
Частые ошибки
Использовать async для побочного действия — попадёте на CancellationException-стирание (исключения откладываются до await).
Не использовать await на Deferred — результат пропадает.
Дополнительные вопросы
Что произойдёт с исключением в async, если не вызвать await?
Зачем нужны coroutineScope и supervisorScope?
Что такое sealed class и для чего он нужен?
Middle
sealed class — это класс с ограниченным набором наследников, объявленных в том же файле или модуле. Компилятор знает обо всех вариантах.
При использовании в when можно не писать else: компилятор проверит, что покрыты все ветки. Это даёт исчерпывающую обработку и помогает при добавлении нового варианта — компилятор укажет на все when, где его не обработали.
Из практики: классический инструмент для представления результатов с разными «формами» (Success, Error, Loading), команд, состояний. Лучше иерархии чем enum, потому что каждый вариант может нести свои данные.
Частые ошибки
Думать, что наследников можно объявить где угодно — в Kotlin 1.5+ они в том же модуле, в 1.0 — в том же файле.
Заменять sealed class enum-ом, когда нужны разные поля у вариантов.
Дополнительные вопросы
Чем sealed interface отличается от sealed class?
Когда sealed class лучше enum?
Что такое when и чем он отличается от switch в Java?
Junior
when — это конструкция выбора по значению. В отличие от switch в Java, она может быть выражением (возвращать значение), не требует break, поддерживает любые типы и сложные условия в ветках.
Можно матчить интервалы (in 1..10), типы (is String), произвольные предикаты (when { x > 0 -> ... }) и проверять на сразу несколько значений через запятую.
Из практики: when заменяет if/else if/else цепочки и читается гораздо чище. Особенно силен с sealed классами — даёт исчерпывающую проверку.
Частые ошибки
Использовать when без сохранения значения, когда оно нужно — теряется выгода.
Писать слишком сложные условия в ветке — лучше вынести в функцию.
Дополнительные вопросы
Можно ли в when использовать диапазоны?
Что такое exhaustive when?
Что такое companion object и аналог ли это static в Java?
Middle
В Kotlin нет ключевого слова static. Для «статичных» членов используют companion object — единственный объект, привязанный к классу. Доступ из кода: ClassName.member.
В отличие от static, companion object — настоящий объект, он может реализовывать интерфейсы, его можно передавать как значение. Для совместимости с Java добавляют аннотации @JvmStatic, @JvmField.
Из практики: companion object используют для фабрик, констант, фабричных методов. Если нужно вызывать из Java как обычный static — обязательно @JvmStatic.
Частые ошибки
Звать из Java без @JvmStatic — придётся писать ClassName.Companion.method().
Класть в companion object тяжёлое состояние — это в сущности глобальный singleton.
Дополнительные вопросы
Чем object отличается от companion object?
Когда уместен @JvmField?
Что такое object declaration в Kotlin?
Middle
object — это объявление одиночного объекта (singleton). object Repository { fun load() {...} } — один экземпляр, к которому обращаются по имени.
Также object используют для анонимных объектов (вместо anonymous inner class в Java): val listener = object : Listener { override fun onClick() {...} }.
Из практики: для классических синглтонов в Kotlin не нужны паттерны на double-checked locking — object безопасно создаётся лениво при первом обращении.
Частые ошибки
Делать object с тяжёлой инициализацией — она пройдёт при первом вызове, может стать сюрпризом в горячем пути.
Использовать object для сервисов, которые нужно подменять в тестах — без интерфейсов сложно.
Дополнительные вопросы
Чем object отличается от class?
Безопасен ли object с точки зрения многопоточности?
Что такое smart cast и когда он работает?
Middle
Smart cast — это автоматическое приведение типа после проверки. Если компилятор видит is String, дальше в ветке переменная уже имеет тип String без явного приведения.
Работает для локальных переменных, val-свойств без custom getter и параметров функций. Не работает для var-свойств открытого класса или mutable свойств из другого модуля, потому что между проверкой и использованием значение может измениться.
Из практики: упрощает работу с when (is ...) -> и null-проверками. Когда smart cast не срабатывает (например, для var), компилятор сам подскажет, что нужно сохранить в локальную val.
Частые ошибки
Удивляться, почему после if (x != null) с var-полем класса всё равно компилятор требует !!.
Полагаться, что smart cast пройдёт через инлайн-функцию из другой библиотеки.
Дополнительные вопросы
Что делать, если smart cast не работает?
Чем as отличается от is?
Что такое inline function и зачем она нужна?
Middle
inline у функции говорит компилятору: подставлять её тело в место вызова, а не делать обычный вызов. Особенно полезно для функций с лямбдой-параметром — лямбда инлайнится тоже, и нет аллокации объекта-лямбды.
Это даёт нулевую накладную стоимость для функций типа let, run, forEach, и позволяет внутри inline-функции делать non-local return из вызывающего кода.
Из практики: писать свои inline-функции имеет смысл, когда они принимают лямбды и вызываются часто. Просто все подряд функции inline-ить вредно — раздувается код.
Частые ошибки
Помечать inline функции без лямбд — выгода почти нулевая, а размер кода растёт.
Inline-функцию нельзя сделать virtual и нельзя обращаться к её приватным членам класса извне.
Дополнительные вопросы
Что такое crossinline и noinline?
Чем reified type parameter полезен?
Что такое reified type parameter в Kotlin?
Middle
Обычно дженерики на JVM стираются — внутри функции нельзя узнать конкретный T. Но в inline-функции, если параметр пометить как reified, компилятор подставит тип на этапе компиляции, и его можно использовать как обычный класс.
Пример: inline fun <reified T> parse(json: String): T = mapper.readValue(json, T::class.java). Снаружи parse<User>(json) автоматически передаст класс User.
Из практики: убирает шаблонный код вида readValue(json, User::class.java). Особенно ценно в утилитах сериализации и DI.
Частые ошибки
Reified работает только с inline-функцией — не получится использовать в обычной.
Звать reified-функцию из Java — не сработает, требуется передавать класс явно.
Дополнительные вопросы
Какие ограничения на reified?
Чем reified лучше KClass-параметра?
Что такое property delegation (by lazy, by Delegates.observable)?
Middle
Делегирование свойства — это механизм, передающий get/set другому объекту через ключевое слово by. val name: String by lazy { computeName() } — значение вычисляется при первом обращении.
Встроенные делегаты: lazy (ленивая инициализация), Delegates.observable (уведомлять на каждое изменение), Delegates.vetoable (разрешать/запрещать изменение), map (читать из Map).
Из практики: by lazy — типовое решение для тяжёлых вычислений и кешей. observable — для UI и реактивных моделей. Также можно писать свои делегаты, реализуя getValue/setValue.
Частые ошибки
Использовать lazy в многопоточной среде с режимом NONE и получать гонки.
Создавать тяжёлые делегаты на горячем пути — overhead.
Дополнительные вопросы
Какие режимы lazy thread safety бывают?
Как написать собственный делегат?
Чем enum class в Kotlin отличается от sealed class?
Middle
enum — это набор фиксированных именованных значений. Каждое значение — один и тот же тип, и у всех значений одинаковый набор полей. Подходит для конечного набора констант (Color, Status).
sealed class — это набор ограниченных подтипов, у каждого может быть свой состав полей и поведение. Подходит, когда «варианты» разнообразны (Loading, Success(data), Error(message)).
Из практики: если каждый вариант — просто метка с одинаковой структурой, выбирайте enum; если варианты несут разные данные — sealed. Их можно сочетать: enum-класс может реализовывать sealed interface (но не наследовать sealed class — у enum уже есть неявный родитель Enum), а внутри sealed class можно объявить вложенный enum.
Частые ошибки
Втискивать в enum поля, которые есть только у некоторых вариантов.
Использовать sealed class там, где enum читался бы яснее.
Дополнительные вопросы
Можно ли у enum переопределять методы у конкретного значения?
Как сериализовать sealed class в JSON?
Что такое деструктуризация в Kotlin?
Middle
Деструктуризация позволяет распаковать объект в отдельные переменные: val (name, age) = user. Работает с data class (там componentN сгенерирован автоматически) и с любым типом, у которого есть operator fun componentN.
Удобно в циклах и при работе с парами/мапами: for ((key, value) in map) { ... }. Игнорировать поле можно через _: val (a, _, c) = triple.
Из практики: помогает делать код лаконичным при работе с DTO и tuple-подобными типами. Особенно при возврате нескольких значений из функции.
Частые ошибки
Полагаться на порядок component1/component2/... — при изменении data class порядок меняется.
Деструктурировать большие объекты ради 2-3 полей — теряется читаемость.
Дополнительные вопросы
Что такое componentN и кто его генерирует?
Как сделать деструктуризацию для обычного класса?
Что такое typealias и для чего он?
Middle
typealias задаёт синоним для существующего типа: typealias UserId = String или typealias Handler<T> = (T) -> Unit. Не создаёт новый тип — это именно псевдоним.
Полезен для семантической читаемости (UserId вместо String), для сокращения сложных дженериков, для упрощения функциональных сигнатур.
Из практики: typealias не заменяет value class и не даёт защиты типизации — UserId и любая String взаимозаменяемы. Для строгой типизации нужен value class (inline class).
Частые ошибки
Думать, что typealias делает новый тип — это не так, нет защиты от перепутывания.
Засорять кодбазу typealias-ами для тривиальных типов.
Дополнительные вопросы
Чем typealias отличается от value class?
Когда лучше использовать value class?
Что такое extension property в Kotlin?
Middle
Extension property — это свойство, добавленное к типу как расширение, по аналогии с extension function. val String.lastChar: Char get() = this[length - 1].
У него не может быть backing field — это всегда вычисляемое свойство, реализующееся через get (и set, если var).
Из практики: хороший способ добавить «инструкции для чтения» к типу — например, val List<T>.secondOrNull: T?. Просит ввести вспомогательную переменную, оставаясь компактным синтаксически.
Частые ошибки
Пытаться объявить var-extension с хранилищем — не получится.
Перегружать имя стандартного свойства — может ввести в заблуждение.
Дополнительные вопросы
Можно ли сделать extension property для nullable типа?
Где лучше: extension function или extension property?
Что такое coroutine scope и зачем нужен structured concurrency?
Middle
CoroutineScope ограничивает время жизни корутин: при отмене scope отменяются и все его дочерние корутины. Это и есть структурный параллелизм — гарантия, что не остаётся «забытых» работ.
В Android есть готовые scope: viewModelScope, lifecycleScope. На бэкенде scope привязывают к запросу, юниту работы, тесту. Внутри функций используют coroutineScope { ... } для подзадач.
Из практики: благодаря structured concurrency сложно создать утечку — каждая корутина имеет родителя и заканчивается с ним. Это резко проще, чем ручное управление потоками.
Частые ошибки
Запускать корутину в GlobalScope в проде — теряется привязка к жизненному циклу.
Не учитывать, что отмена родителя отменяет всех детей — это поведение by design.
Дополнительные вопросы
Чем supervisorScope отличается от coroutineScope?
Что такое Job и CoroutineContext?
Что такое Flow в Kotlin coroutines?
Middle
Flow — это асинхронная последовательность значений, аналог Observable из RxJava, но интегрированный в корутины. Производитель эмитит через emit, потребитель собирает через collect.
Cold by default — Flow ничего не делает, пока его не collect. У него богатый набор операторов: map, filter, debounce, flatMapConcat, retry, take. Поддерживает backpressure естественным образом.
Из практики: Flow используют для потоков данных (события UI, push из бэкенда, БД-наблюдатели). StateFlow и SharedFlow — горячие версии для состояния и событий.
Частые ошибки
Собирать (collect) Flow в неподходящем scope — утечка.
Считать Flow таким же как Sequence — это асинхронные потоки, у них разная семантика.
Дополнительные вопросы
Чем StateFlow отличается от SharedFlow?
Что такое backpressure и как Flow с ним работает?
Чем Kotlin интероперабелен с Java и какие подводные камни?
Middle
Kotlin компилируется в обычный JVM-байткод, поэтому Java-классы и Kotlin-классы могут вызывать друг друга. Большая часть API Kotlin доступна из Java напрямую.
Подводные камни: platform-типы (Java возвращает то, что может быть null, Kotlin видит это как T! и не проверяет), companion object из Java виден как ClassName.Companion (нужен @JvmStatic), extension function видна как статический метод утилитного класса, дефолтные значения параметров требуют @JvmOverloads.
Из практики: при работе с Java-API расставляйте @Nullable/@NotNull в Java или принимайте на стороне Kotlin String? для безопасности. Для библиотек, которые будут звать из Java, добавляйте @Jvm-аннотации.
Частые ошибки
Полагаться на platform-тип как на ненулевой и ловить NPE.
Забыть @JvmOverloads и не получить overloads в Java.
Дополнительные вопросы
Что такое platform-тип?
Какие аннотации @Jvm... самые полезные?
Когда использовать data class, value class и обычный class?
Middle
data class — для носителей данных с автогенерируемыми equals/hashCode/toString/copy. Подходит для DTO и моделей.
value class (раньше inline class) — обёртка над одним полем без накладных расходов рантайма. Идеально для семантических обёрток (UserId, Email) с защитой типизации.
Обычный class — для всего остального: сервисы, сущности с identity, классы с поведением. Из практики: для сущностей с жизненным циклом избегайте data class — равенство по значению сбивает с толку. value class — мощное средство против case «передал email вместо username».
Частые ошибки
Делать value class и забыть, что в некоторых случаях он всё равно создаст объект (например, в дженерике).
Использовать data class для бизнес-сущности и сравнивать её по содержимому, а не по id.
Дополнительные вопросы
Что такое boxing у value class?
Можно ли у value class иметь несколько полей?
Что такое типы Nothing и Unit в Kotlin?
Middle
Unit — это аналог void в Java: функция ничего не возвращает (точнее, возвращает единственное значение Unit). По умолчанию подразумевается, если возвращаемый тип не указан.
Nothing — тип, означающий «функция не возвращается нормально» (бросает исключение или бесконечный цикл). Используется как маркер для компилятора.
Из практики: Nothing полезен в утилитах throw fail("msg"): val x: String = nullableValue ?: fail("..."). Компилятор знает, что после fail значение не вернётся.
Частые ошибки
Путать Unit и Void — Void из Java сюда не подходит.
Использовать Nothing как обычный тип.
Дополнительные вопросы
Что такое Nothing? (nullable Nothing)?
Чем Unit отличается от void в Java?
Что такое open и final в Kotlin?
Middle
По умолчанию классы и методы в Kotlin final — нельзя наследовать/переопределять. Чтобы разрешить — open перед объявлением.
Это противоположно Java, где по умолчанию всё «открыто». Идея — безопаснее по умолчанию: явно выбираем, что можно переопределить.
Из практики: для тестируемости (mock-фреймворки) иногда мешает — приходится либо open, либо использовать allopen-плагин Gradle.
Частые ошибки
Использовать наследование вместо композиции, потому что «привычно».
Забывать open и удивляться, что не получается переопределить метод.
Дополнительные вопросы
Что такое allopen Gradle plugin?
Чем abstract отличается от open?
Что такое visibility modifiers в Kotlin?
Junior
public (по умолчанию — доступен везде), internal (внутри модуля), protected (в классе и наследниках, не для top-level), private (в классе или файле).
internal — особенный: ограничивает доступ модулем (gradle subproject). Удобно для библиотек: внутренняя API не видна снаружи.
Из практики: для библиотек минимальная по умолчанию public — это анти-паттерн. Лучше использовать internal/private для всего, кроме явного публичного API.
Частые ошибки
Делать всё public и потом нельзя менять без breaking change.
Использовать protected для top-level — это синтаксическая ошибка.
Дополнительные вопросы
Что такое модуль в смысле internal?
Чем private отличается на уровне файла и класса?
Что такое infix function в Kotlin?
Middle
Функция, помеченная infix, вызывается без точки и скобок: a func b вместо a.func(b). Должна быть с одним параметром, методом класса или extension.
Используется для удобства DSL: 1 to "a" (создаёт Pair), shouldEqual, until.
Из практики: infix делает код выразительнее в определённых контекстах. Не злоупотребляйте — обычные методы читаются понятнее в большинстве случаев.
Частые ошибки
Делать infix-функцию с непонятным именем — снижает читаемость.
Использовать infix там, где имя функции не вяжется со словесной конструкцией.
Дополнительные вопросы
Что такое DSL в Kotlin?
Чем infix отличается от operator?
Что такое operator function в Kotlin?
Middle
operator перед функцией позволяет переопределить операторы: plus → +, minus → -, get → [], invoke → (). Например, operator fun Point.plus(other: Point) даёт p1 + p2.
Имена операторов стандартизованы — каждый соответствует своей сигнатуре.
Из практики: незаменимо для математических классов, DSL, контейнеров. Без злоупотреблений — делает API натуральнее.
Частые ошибки
Перегружать оператор там, где смысл операции неочевиден.
Забыть, что some.contains -> можно как in, но порядок аргументов важен.
Дополнительные вопросы
Что такое compareTo и Comparable?
Чем componentN связан с деструктуризацией?
Что такое generics in/out (variance) в Kotlin?
Middle
out T — ковариантный параметр (T только в позициях производства, нельзя в потреблении). in T — контравариантный (только в потреблении).
Пример: List<out T> — можно List<String> присвоить переменной типа List<Any>, потому что только читаем. MutableList<in T> — наоборот, можно писать.
Из практики: правило PECS — Producer Extends (out), Consumer Super (in). Понимание помогает писать гибкие API без warnings.
Частые ошибки
Перепутать in и out — компилятор не даст использовать в неправильной позиции.
Использовать out для mutable collection — нельзя добавить элементы.
Дополнительные вопросы
Что такое star-projection (List<*>)?
Чем reified отличается от обычного дженерика?
Что такое typealias и когда он полезен?
Middle
typealias задаёт синоним для существующего типа: typealias UserId = String, typealias Handler = (Event) -> Unit. Не создаёт новый тип — это псевдоним.
Полезен для документации (UserId говорит больше, чем String), для сокращения сложных функциональных сигнатур.
Из практики: typealias не даёт типобезопасности — UserId и любая String взаимозаменяемы. Для строгой типизации — value class.
Частые ошибки
Считать, что typealias защищает от смешивания типов.
Засорять кодбазу алиасами для тривиальных типов.
Дополнительные вопросы
Чем typealias отличается от value class?
Когда лучше использовать value class?
Что такое with vs apply vs also vs let?
Middle
with(obj) { ... } — не extension, возвращает результат лямбды, this — obj. apply { ... } — extension, возвращает сам объект, this — obj. also { ... } — extension, возвращает объект, it — объект. let { ... } — extension, возвращает результат, it — объект.
Выбор: для конфигурации с возвратом — apply. Для побочного действия — also. Для null-safe вызова — let. Для группировки операций — with.
Из практики: путаница между ними — обычное дело. Главное — последовательность внутри проекта.
Частые ошибки
Использовать одну функцию вместо другой и получить неожиданный возвращаемый тип.
Вкладывать scope-функции глубоко — становится непонятно.
Дополнительные вопросы
Какие принципы выбора между ними?
Что такое run?
Что такое coroutine context и dispatchers?
Middle
CoroutineContext — это набор элементов корутины: Job, dispatcher, name. Dispatcher определяет, на каком потоке (или пуле) исполняется корутина: Default (CPU), IO (блокирующий IO), Main (UI), Unconfined.
withContext(Dispatchers.IO) переключает выполнение на IO-пул. После withContext возвращается на исходный.
Из практики: для сетевых запросов и БД — IO. Для CPU-bound — Default. UI-обновления — Main. Не использовать Main для тяжёлых операций.
Частые ошибки
Запускать блокирующий IO на Dispatchers.Default — забивается пул.
Не использовать Main.immediate на UI — лишний переключатель потока.
Дополнительные вопросы
Чем Main отличается от Main.immediate?
Что такое coroutineScope и withContext?
Что такое exception handling в корутинах?
Middle
В launch исключение пробрасывается родителю scope и (если не обработано) распространяется выше. В async исключение хранится в Deferred и выкидывается на await.
CoroutineExceptionHandler можно добавить в context, чтобы перехватить unhandled exception в launch. SupervisorJob изолирует братьев — упал один не отменит других.
Из практики: всегда явно обрабатывайте исключения в launch — иначе они уничтожают весь scope. В async помните про await.
Частые ошибки
Полагаться на try/catch вокруг launch — он не словит исключение внутри корутины.
Использовать обычный Job там, где нужен SupervisorJob.
Дополнительные вопросы
Чем supervisorScope отличается от coroutineScope?
Что такое CancellationException?
Что такое sealed interface (Kotlin 1.5+)?
Middle
sealed interface — то же, что sealed class, но интерфейс. Можно использовать для иерархий с множественным наследованием.
Идеален для алгебраических типов: sealed interface Result<T>; class Success<T>(val value: T): Result<T>; class Failure(val error: String): Result<Nothing>.
Из практики: для современного Kotlin sealed interface часто предпочтительнее sealed class, потому что можно комбинировать с другими интерфейсами.
Частые ошибки
Использовать sealed class там, где можно interface — теряется гибкость наследования.
Не учитывать exhaustive when при добавлении нового варианта.
Дополнительные вопросы
Чем sealed interface отличается от sealed class?
Можно ли иметь sealed interface, наследующий обычный interface?
Что такое context receivers?
Middle
Context receivers (экспериментально в Kotlin 1.6+) позволяют функции «требовать» наличия определённого контекста: context(Logger) fun process() { log(...) }.
Это шаг к более удобному DI и DSL — без явной передачи параметров.
Из практики: пока экспериментально, в production code лучше использовать осторожно. Может пригодиться в библиотеках с богатым DSL.
Частые ошибки
Использовать в production кодах нестабильную фичу.
Считать context receivers заменой DI.
Дополнительные вопросы
Чем context receivers отличаются от extension functions?
Когда стабилизация ожидается?
Что такое Channel в корутинах?
Middle
Channel — это асинхронная очередь между корутинами. Send и receive — suspend-функции. Поддерживает buffering, conflated (последнее значение), broadcast.
Похоже на Channel в Go. Используется для producer-consumer паттерна.
Из практики: для большинства сценариев достаточно Flow. Channel — когда нужна явная очередь между несколькими producer/consumer.
Частые ошибки
Забыть закрыть Channel — receive зависнет.
Использовать Channel для одиночного значения — Flow или Deferred удобнее.
Дополнительные вопросы
Чем Channel отличается от Flow?
Что такое actor в корутинах?
Что такое DSL в Kotlin и как его строить?
Middle
Kotlin DSL — domain-specific language внутри Kotlin. Строится через lambda with receiver, extension functions, operator functions, infix.
Из практики: для специфичных задач DSL даёт более выразительный код. Но требует осторожности — слишком сложный DSL непонятен новичкам.
Частые ошибки
Перебарщивать с magic — DSL должен быть прозрачным.
Не использовать @DslMarker и получать конфликт this.
Дополнительные вопросы
Что такое @DslMarker?
Чем lambda with receiver отличается от обычной?
Что такое multiplatform Kotlin?
Middle
KMP (Kotlin Multiplatform) — кодшеринг между Android, iOS, JVM, JS, Native. Общая бизнес-логика, специфичный UI для каждой платформы.
Compose Multiplatform — общий UI для Android, iOS, desktop, web (экспериментально).
Из практики: KMP — серьёзный кандидат для команд, разрабатывающих под несколько платформ. Зрелость растёт, продакшен-кейсы — Cash App, Philips Hue, Netflix.
Частые ошибки
Полагать, что KMP — это для всего UI.
Использовать платформенный код там, где можно общий.
Дополнительные вопросы
Чем KMP отличается от Flutter?
Что такое expect/actual?
Что такое value class (inline class)?
Middle
value class (раньше inline class) — обёртка вокруг одного поля, не создающая объект в runtime для большинства случаев. Идеально для семантических обёрток: UserId над String, Email над String.
Даёт типобезопасность (нельзя перепутать UserId и Email) без overhead.
Из практики: для строгой типизации — отличный инструмент. В некоторых случаях (generics, nullable) всё-таки создаётся объект (boxing) — нужно понимать.
Частые ошибки
Использовать value class с несколькими полями — пока нельзя.