Kotlin: выстрел в яблочко или мимо кассы?
Kotlin — это не просто «Java без бойлерплейта». Это язык, который затащил Google в своё сердце, обогнал Swift по темпам роста и стал обязательным пунктом в вакансиях мобильных разработчиков. Но вот парадокс: многие приходят на собеседование с мыслью «я знаю Java — разберусь», а уходят с красными глазами. Потому что Kotlin — это другой мир: без null, с корутинами и функциями-расширениями, которые ломают шаблоны.
Мы в PythonLib (да, мы про Python, но Kotlin — это must have для современного бэкенда и мобилок) собрали реальный кейс: как пройти собеседование по Kotlin с блеском. Никакой воды — только код, логика и секретные фишки, о которых молчат на курсах.
1. Почему Kotlin — не «просто Java 2.0»?
Первый вопрос, который вам зададут: «В чём принципиальное отличие Kotlin от Java?». Если вы скажете «синтаксис короче» — вас вежливо попросят выйти. Главное отличие — null safety на уровне типов. В Java вы можете забыть проверить объект на null и получить NPE в рантайме. В Kotlin компилятор не даст вам даже скомпилировать код, если есть риск null.
// Java
String name = null;
int length = name.length(); // NPE в рантайме
// Kotlin
var name: String? = null
val length = name?.length ?: 0 // Безопасно: length = 0
Но это — база. Интервьюер копнёт глубже: спросит про !! (non-null assertion), оператор Элвиса ?: и безопасные вызовы ?.. Ваша задача — показать, что вы понимаете: !! — это зло, а ?: — ваш друг.
- Safe call (?.) — если объект null, возвращает null.
- Elvis (?:) — если слева null, берём значение справа.
- !! — используйте только если уверены на 100%, иначе — баг.
2. Extension functions: магия, которую нужно объяснить
Вопрос: «Как добавить метод к классу, который вы не можете изменить?». В Java — паттерн Decorator или Util-класс. В Kotlin — extension functions. Это не реальное изменение класса, а синтаксический сахар, который делает код читаемым.
// Extension function для String
fun String.isEmail(): Boolean {
return this.contains("@") && this.contains(".")
}
// Использование
val email = "user@pythonlib.ru"
println(email.isEmail()) // true
Интервьюер может спросить: «Можно ли переопределить extension function?». Ответ: нет, это статический вызов, не динамический. Если вы объявите функцию с тем же именем в классе, приоритет у метода класса.
3. Sealed class vs Enum: когда что использовать?
Это классический вопрос на middle-уровне. Enum — для фиксированного набора констант (дни недели, статусы). Sealed class — для ограниченной иерархии типов, где каждый элемент может иметь своё состояние.
sealed class Result {
data class Success(val data: String) : Result()
data class Error(val code: Int, val message: String) : Result()
object Loading : Result()
}
fun handleResult(result: Result) {
when (result) {
is Result.Success -> println("Data: ${result.data}")
is Result.Error -> println("Error ${result.code}: ${result.message}")
Result.Loading -> println("Loading...")
}
}
Почему это важно? Потому что when с sealed class — это гарантия, что вы обработали все случаи. Компилятор не даст вам забыть про новый тип, если вы добавите его в sealed class.
4. Корутины: как не провалиться в асинхронность
Тема, на которой ломаются 70% кандидатов. Корутины — это не потоки. Это легковесные «нити», которые могут приостанавливаться без блокировки потока.
suspend fun fetchData(): String {
delay(1000) // Не блокирует поток!
return "Data from PythonLib"
}
fun main() = runBlocking {
launch {
val result = fetchData()
println(result)
}
println("This runs first!")
}
Интервьюер спросит: «Чем отличается launch от async?». launch — запускает корутину, возвращает Job (без результата). async — возвращает Deferred (с результатом через .await()).
- runBlocking — только для main функции и тестов.
- Dispatchers.Default — для CPU-интенсивных задач.
- Dispatchers.IO — для сети и диска.
- Dispatchers.Main — для UI (Android).
5. Дженерики: in, out, star projection
Вопрос: «Как сделать ковариантный List в Kotlin?». В Java — List<? extends T>, в Kotlin — List<out T>. Но Kotlin пошёл дальше: есть declaration-site variance.
interface Source<out T> {
fun nextT(): T // Можно возвращать T
// fun addT(item: T) // Ошибка! in position запрещён
}
interface Sink<in T> {
fun add(item: T) // Можно принимать T
// fun get(): T // Ошибка! out position запрещён
}
Запомните: out — только чтение (производство), in — только запись (потребление). Star projection (*) — «я не знаю тип, но это безопасно».
6. Проверка на прочность: рефлексия и inline-функции
Рефлексия в Kotlin — это KClass и KFunction. Пример:
val kClass = String::class
val functions = kClass.members
println(functions) // Список всех методов String
Но главная фишка — inline-функции с reified типами. Без них вы не можете проверить тип параметра в дженерике во время выполнения.
inline fun <reified T> isType(value: Any): Boolean {
return value is T
}
fun main() {
println(isType<String>("Hello")) // true
println(isType<Int>("Hello")) // false
}
Без reified компилятор бы стёр тип. Это must-know для тех, кто пишет библиотеки.
7. Финальный аккорд: scope functions (let, run, with, apply, also)
Интервьюер обожает эту тему. Разница между ними — в контексте (this vs it) и возвращаемом значении.
- let — работает с
it, возвращает последнее выражение. Исп