Урок по треку Kotlin 7 мин

Классы, data и sealed за 7 минут

Конструкторы, data class, sealed-иерархии, object, enum, value class и видимость.

Классы в Kotlin короче, чем в Java, но под капотом те же JVM-классы. На собесе спрашивают, что генерирует компилятор и как выбирать между data, sealed, enum и object.

Конструкторы и свойства

class User(val id: Long, var name: String = "Гость") {
    init { require(id > 0) { "id должен быть положительным" } }

    constructor(name: String) : this(1, name)   // вторичный конструктор

    val isGuest: Boolean
        get() = name == "Гость"                   // вычисляемое свойство
}

val/var в первичном конструкторе сразу объявляют свойства. Свойство это не поле: у него есть геттер (и сеттер у var), а поле (field) создаётся, только если оно нужно. Блоки init и инициализаторы свойств выполняются сверху вниз в порядке объявления.

final по умолчанию

Классы и методы в Kotlin final. Чтобы наследоваться, нужен open (или abstract), для переопределения override. Это защищает от хрупкого базового класса. Spring и подобные фреймворки, которым нужны прокси, решают это плагином kotlin-allopen (kotlin-spring).

Видимость: public по умолчанию, private, protected, и internal (виден в пределах модуля Gradle). Пакетной видимости, как в Java, нет.

data class

data class Point(val x: Int, val y: Int)

val p = Point(1, 2)
val q = p.copy(y = 5)        // Point(x=1, y=5)
val (x, y) = p               // деструктуризация
println(p == Point(1, 2))    // true

Компилятор генерирует equals, hashCode, toString, copy и componentN только по свойствам первичного конструктора. Свойство, объявленное в теле класса, в equals не участвует. Требования: в первичном конструкторе хотя бы один параметр, все параметры val или var, класс не может быть open, abstract, sealed или inner.

copy делает поверхностную копию: вложенный MutableList окажется общим у оригинала и копии.

Частая ошибка: data class с var-полями как ключ HashMap. Изменили поле, хеш изменился, и элемент «потерялся» в мапе.

sealed-классы и интерфейсы

Закрытая иерархия: все прямые наследники известны компилятору (они в том же модуле и пакете).

sealed interface UiState {
    data object Loading : UiState
    data class Content(val items: List<String>) : UiState
    data class Error(val message: String) : UiState
}

fun render(s: UiState) = when (s) {
    UiState.Loading -> "..."
    is UiState.Content -> "${s.items.size} шт."
    is UiState.Error -> s.message
}   // else не нужен: when полный

Чем sealed лучше enum: у каждого варианта могут быть свои поля и сколько угодно экземпляров. Чем лучше обычного наследования: when проверяется на полноту, добавили вариант, и компилятор покажет все места, где его забыли обработать.

enum

enum class Status(val code: Int) {
    ACTIVE(1), BLOCKED(2);
    fun isOk() = this == ACTIVE
}
Status.valueOf("ACTIVE"); Status.entries   // entries вместо values() с Kotlin 1.9

Enum подходит, когда варианты одинаковы по структуре и это синглтоны.

object и companion object

  • object Config { } это синглтон, создаётся лениво и потокобезопасно при первом обращении.
  • companion object это объект внутри класса, аналог статических членов: User.create(). На JVM его члены не статические, если не добавить @JvmStatic.
  • object : Listener { } это выражение-объект, анонимный класс.
  • data object (Kotlin 1.9+) даёт синглтону красивый toString и корректный equals, удобно в sealed-иерархиях.

value class

@JvmInline
value class UserId(val value: Long)

Обёртка для типобезопасности без накладных расходов: в большинстве мест компилируется в голый Long. Упаковка всё же происходит, когда значение используется как nullable, как дженерик или как интерфейс.

Вложенные и внутренние классы

class Outer { class Nested } это статический вложенный класс, ссылки на внешний нет. inner class хранит ссылку на внешний объект (как нестатический внутренний в Java), и это частая причина утечек памяти в Android.

Частые ошибки

  • Ожидать, что свойство из тела data class участвует в equals.
  • Использовать изменяемый data class как ключ HashMap или элемент HashSet.
  • Думать, что copy глубокая.
  • Забыть open и удивляться ошибке «This type is final».
  • Писать inner class там, где хватает вложенного.

Что спросят на собесе

  • Что генерирует data class и какие у него ограничения?
  • Чем sealed-класс отличается от enum и abstract-класса?
  • Как в Kotlin сделать синглтон и статический метод?
  • Почему классы по умолчанию final?
  • Что такое value class и когда он всё-таки упаковывается?

Закрепите за 5 минут

После урока в тренажёре 3-5 проверочных вопросов, дальше модуль уходит в повторения.