Введение: почему Kotlin кажется лёгким, но подкидывает сюрпризы
Kotlin — современный, лаконичный и невероятно удобный язык. Многие новички приходят в него из Java или Python и думают: «Ну, тут же всё понятно!». И действительно, синтаксис Kotlin чистый, nullable типы защищают от NPE, а функциональные фичи вроде let, apply и run выглядят магически просто. Но именно эта кажущаяся простота часто становится ловушкой.
В этой статье мы разберём самые распространённые ошибки при изучении Kotlin, которые совершают новички. Вы узнаете, как их избежать, и получите готовые примеры кода. Если вы только начинаете свой путь в изучении Kotlin для начинающих, эта статья спасёт вас от многих часов дебага.
Ошибка №1: Игнорирование null safety или, наоборот, паранойя
Kotlin вшил null safety прямо в систему типов. Это благо, но новички часто впадают в две крайности. Первая — полное игнорирование и использование оператора !! (not-null assertion) на каждом шагу. Вторая — чрезмерное использование элвис-оператора ?: и цепочек из ?.let, которые превращают код в нечитаемую кашу.
Как выглядит ошибка
// Плохо: тотальный !! — убивает всю безопасность Kotlin
fun processUser(user: User?) {
val name = user!!.name // Если user == null — краш!
println(name.uppercase())
}
// Плохо: слишком много вложенных let
fun getUserCity(user: User?): String? {
return user?.let {
it.address?.let { addr ->
addr.city?.let { city ->
city.name
}
}
}
}
Как правильно
// Хорошо: используем безопасный вызов + элвис
fun processUserSafe(user: User?) {
val name = user?.name ?: "Гость"
println(name.uppercase())
}
// Отлично: расширения и scope-функции без зоопарка вложенности
fun getUserCityClean(user: User?): String? {
return user?.address?.city?.name
}
Совет: Оператор !! используйте только если вы на 100% уверены, что значение не null (например, после проверки в if). В остальных случаях — ?. и ?:.
Это одна из главных проблем Kotlin новичков — они либо боятся null, либо не понимают, как с ним работать. Помните: Kotlin не запрещает null, он заставляет вас думать о нём.
Ошибка №2: Непонимание разницы между var, val и константами
Кажется, что val — это финальная переменная, как final в Java. Но нет. val — это read-only ссылка, а не неизменяемый объект. Новички часто путают val с константой времени компиляции (const val) и удивляются, почему их код работает не так.
Пример путаницы
// Новичок думает: "val = неизменяемо"
val list = mutableListOf(1, 2, 3)
list.add(4) // Работает! Но новичок в шоке
// А вот это уже ошибка компиляции
val name = "Alice"
name = "Bob" // Ошибка: val не может быть переприсвоено
Как отличить
// Константа времени компиляции — только для примитивов и String
const val PI = 3.14159
// Read-only ссылка — может быть вычислена в runtime
val currentTime = System.currentTimeMillis()
// Настоящая неизменяемость — используйте immutable коллекции
val immutableList = listOf(1, 2, 3) // listOf, а не mutableListOf
Золотое правило: Используйте val везде, где возможно. Если переменная должна меняться — только тогда var. А const val — только для констант верхнего уровня или внутри object/companion object.
Ошибка №3: Злоупотребление companion object и object-синглтонами
Kotlin предлагает элегантный способ создания синглтонов через object. Но новички часто делают companion object для всего подряд, превращая его в мусорную корзину для статических методов. Это нарушает принципы SOLID и делает код сложным для тестирования.
Типичный антипаттерн
class UserService {
companion object {
fun validateEmail(email: String): Boolean { /* ... */ }
fun formatName(name: String): String { /* ... */ }
const val DEFAULT_AVATAR = "default.png"
fun createInstance(): UserService = UserService()
}
}
Что делать вместо этого
// Для утилитарных функций — extension функции или top-level
fun String.isValidEmail(): Boolean {
return this.contains("@")
}
// Для констант — top-level const val
const val DEFAULT_AVATAR = "default.png"
// Для фабричных методов — отдельный object или функция
object UserServiceFactory {
fun create(): UserService = UserService()
}
// Для DI — используйте Koin/Hilt, не синглтоны вручную
Совет: companion object — не место для всего. Используйте его только для членов, которые тесно связаны с классом (например, фабричные методы или константы, специфичные для класса).
Это частая ошибка Kotlin, которая перекочевала из Java-мышления. В Kotlin лучше использовать top-level функции и extension функции — они более гибкие.
Ошибка №4: Путаница между == и ===, а также игнорирование data class
В Java == сравнивает ссылки, а equals() — содержимое. В Kotlin == — это структурное равенство (вызывает equals()), а === — ссылочное. Новички, пришедшие из Java, часто используют == для сравнения строк и получают неожиданные результаты, если не переопределён equals. Но есть и обратная сторона: они забывают про data class, который автоматически генерирует equals, hashCode, toString и copy.
Ошибка и её исправление
// Новичок пишет обычный класс и мучается
class Person(val name: String, val age: Int)
fun main() {
val p1 = Person("Alice", 25)
val p2 = Person("Alice", 25)
println(p1 == p2) // false! equals не переопределён
}
// Профи использует data class
data class Person(val name: String, val age: Int)
fun main() {
val p1 = Person("Alice", 25)
val p2 = Person("Alice", 25)
println(p1 == p2) // true! equals сгенерирован автоматически
}