Раздел подготовки

Kotlin: вопросы для собеседования QA

Kotlin полезен QA в мобильных командах: он помогает понимать Android-код, причины багов и ограничения платформы.

41 вопросов 4 базовых 3 уровня: Junior, Middle, Senior

Что закрывает этот раздел

Подготовиться к Kotlin-вопросам для mobile QA и AQA интервью.

  • объяснять null-safety
  • читать Android-код
  • понимать корутины на базовом уровне

С чего начать

Если времени мало, начни с базовых вопросов: они чаще всего помогают пройти первый технический фильтр и показать структуру мышления.

  1. Чем Kotlin отличается от Java?
  2. Чем val отличается от var в Kotlin?
  3. Как работает null-безопасность в Kotlin?
  4. Что такое visibility modifiers в Kotlin?

Вопросы и ответы

Чем Kotlin отличается от Java?

Junior

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.

Примеры: Gradle Kotlin DSL, kotlinx.html, Anko, Ktor routes.

Из практики: для специфичных задач 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 с несколькими полями — пока нельзя.
  • Полагать, что value class всегда бесплатен.

Дополнительные вопросы

  • Чем value class отличается от typealias?
  • Что такое boxing у value class?