Как пройти собеседование по Kotlin: топ вопросов продвинутого уровня
Собеседование на позицию Senior Kotlin-разработчика — это не просто проверка синтаксиса. Интервьюеры хотят увидеть глубокое понимание архитектуры языка, особенностей компиляции и многопоточности. В этой статье мы разберём самые сложные и часто задаваемые вопросы, которые помогут вам уверенно пройти техническое интервью. Материал ориентирован на разработчиков, уже имеющих опыт работы с Kotlin и Java.
1. Корутины: Structured Concurrency и отмена задач
Корутины — одна из сильнейших сторон Kotlin. Продвинутый вопрос часто касается Structured Concurrency. Интервьюер может спросить: «Что произойдёт, если дочерняя корутина выбросит исключение, а родительская её не перехватит?»
Ответ кроется в иерархии Job. Если дочерняя корутина (запущенная через coroutineScope или внутри launch) падает с исключением, оно всплывает к родителю. Родительская корутина отменяется, и все её дочерние задачи тоже. Это делает код предсказуемым и безопасным.
Пример, который стоит показать на собеседовании:
import kotlinx.coroutines.*
fun main() = runBlocking {
try {
coroutineScope {
launch {
delay(100)
throw RuntimeException("Ошибка в дочерней корутине")
}
launch {
delay(200)
println("Эта корутина не выполнится, если первая упадёт")
}
}
} catch (e: Exception) {
println("Исключение перехвачено: ${e.message}")
}
println("Программа продолжает работу")
}
Ещё один частый вопрос — отмена корутин. Как правильно отменить корутину? Используйте Job.cancel() и проверяйте состояние через ensureActive() или isActive внутри длительных операций.
2. Inline-функции и reified-типы
Продвинутые разработчики должны понимать, зачем нужны inline-функции. Вопрос может звучать так: «Почему Kotlin не позволяет использовать reified-типы без inline?»
Ответ: reified (овеществлённые) типы работают только внутри inline-функций, потому что код такой функции встраивается прямо в место вызова. Компилятор подставляет конкретный тип во время компиляции, что невозможно для обычных generic-функций из-за стирания типов в JVM.
Пример, который демонстрирует силу reified:
inline fun <reified T> isInstance(value: Any): Boolean {
return value is T
}
fun main() {
println(isInstance<String>("Hello")) // true
println(isInstance<Int>("Hello")) // false
}
Также спросят про noinline и crossinline. noinline запрещает встраивание конкретного лямбда-параметра, а crossinline запрещает использовать return внутри лямбды (нелокальный возврат).
3. Делегаты: Delegates.observable, vetoable и собственные делегаты
Вопрос про делегаты — классика. Продвинутый уровень подразумевает не только знание встроенных делегатов (lazy, observable), но и умение писать свои.
Пример: «Как реализовать делегат, который логирует каждое изменение свойства?»
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
class LoggingDelegate<T>(private val initialValue: T) : ReadWriteProperty<Any?, T> {
private var value = initialValue
override fun getValue(thisRef: Any?, property: KProperty<*>): T {
println("Чтение ${property.name}: $value")
return value
}
override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
println("Запись ${property.name}: $value")
this.value = value
}
}
class User {
var name: String by LoggingDelegate("Unknown")
}
fun main() {
val user = User()
user.name = "Анна"
println(user.name)
}
Также спросят про Delegates.observable и vetoable. vetoable позволяет отклонить изменение, вернув false в лямбде-обработчике.
4. Sealed-классы и when-выражения: полнота проверки
Sealed-классы — мощный инструмент для моделирования ограниченной иерархии. Вопрос: «В чём разница между sealed class и enum class? Когда что использовать?»
Ответ: enum — это фиксированный набор констант (каждый экземпляр — синглтон). sealed class может иметь разные подклассы с разными состояниями и данными. Kotlin гарантирует, что when-выражение на sealed-классе является исчерпывающим (exhaustive), если вы обработали все подклассы. Это избавляет от лишнего else-блока.
Пример:
sealed class NetworkResult<out T> {
data class Success<T>(val data: T) : NetworkResult<T>()
data class Error(val message: String, val code: Int) : NetworkResult<Nothing>()
object Loading : NetworkResult<Nothing>()
}
fun handleResult(result: NetworkResult<String>) {
when (result) {
is NetworkResult.Success -> println("Успех: ${result.data}")
is NetworkResult.Error -> println("Ошибка: ${result.message} (код ${result.code})")
NetworkResult.Loading -> println("Загрузка...")
}
// Компилятор не требует else, так как все ветки покрыты
}
Продвинутый вопрос: «Можно ли добавить новый подкласс sealed-класса в другом файле?» Нет, все подклассы должны быть объявлены в том же пакете и файле (для sealed class) или в том же модуле (для sealed interface, начиная с Kotlin 1.5).
Заключение
Продвинутое собеседование по Kotlin проверяет не знание синтаксиса, а понимание принципов работы языка: как корутины управляют потоками, как inline-функции влияют на байт-код, как делегаты экономят память, и как sealed-классы делают код безопаснее. Регулярно практикуйтесь на Codewars, чита