Перейти к содержанию

Classes & Types

Классы, объявления объектов и типы-значения Kotlin используются в Android для моделей данных, состояния UI и API. Выбирать тип стоит по его смыслу, а не только по количеству сэкономленного шаблонного кода.

Классы и модели

data class

Для data class обычно генерируются equals(), hashCode(), toString(), copy() и componentN() на основе свойств первичного конструктора. Собственные реализации первых трёх функций или унаследованные final-реализации могут заменять их генерацию.

В первичном конструкторе нужен хотя бы один параметр, причём все его параметры должны быть val или var.

data class User(
    val id: Long,
    val name: String
)

Свойства в теле класса не участвуют в этих сгенерированных функциях. copy() не переносит их текущие значения. Копирование поверхностное: вложенные объекты остаются общими, а не клонируются рекурсивно.

Data class не становится неизменяемым автоматически. Для состояния UI предпочтительны val и неизменяемое содержимое. Изменение свойства, участвующего в вычислении хеша, пока объект служит ключом map или элементом set, нарушает предпосылки корректного поиска.

Data class нельзя объявить open, abstract, sealed или inner, но он может наследовать другой класс и реализовывать интерфейсы. Типичные применения: DTO, доменные модели и состояние UI.

sealed class vs enum class

Enum задаёт фиксированный набор именованных экземпляров. Константы могут иметь свойства и собственное поведение, но не являются отдельными контейнерами для результата каждого запроса.

Sealed-иерархия описывает варианты, способные хранить разные данные:

sealed interface UserResult {
    data class Success(val user: User) : UserResult
    data class Error(val message: String) : UserResult
    data object Loading : UserResult
}

fun label(result: UserResult): String = when (result) {
    is UserResult.Success -> result.user.name
    is UserResult.Error -> result.message
    UserResult.Loading -> "Loading"
}

Прямые подтипы должны быть именованными и находиться в том же пакете и модуле, но не обязательно в том же файле. Открытый подтип без sealed может разрешать дальнейшее наследование за этими границами. В multiplatform-проектах действуют дополнительные ограничения наборов исходников.

Sealed class может хранить общее состояние через конструктор; sealed interface позволяет реализации иметь другой суперкласс. Исчерпывающий when не требует else, если обработаны все варианты; для nullable-входа нужна также ветка null.

Enum подходит для фиксированных вариантов, например порядка сортировки; sealed-типы - для результатов и состояний с разными данными. Для варианта без данных удобен data object: он получает читаемый toString() и структурное равенство, но не copy() или componentN().

Объекты

Ключевое слово object

Выражение object создаёт анонимный экземпляр при каждом выполнении и удобно для разовых реализаций:

val helloWorld = object {
    val hello = "Hello"
    val world = "World"

    override fun toString() = "$hello $world"
}

Объявление object задаёт singleton, инициализируемый при первом обращении. Инициализация потокобезопасна, но последующие операции с изменяемым состоянием не синхронизируются автоматически.

object UserNames {
    fun display(user: User): String = user.name.ifBlank { "Unknown" }
}

На JVM singleton относится к определившему его загрузчику классов, а не ко всем процессам Android. Не храните ссылки на Activity или View в долгоживущих объектах. Глобальное изменяемое состояние также мешает изоляции тестов.

object / companion object / class

Класс позволяет создавать отдельные экземпляры вызовами конструктора. Объявление object предоставляет общий экземпляр. Companion object связан с классом: новый экземпляр внешнего класса не создаёт новый companion.

class MyClass {
    companion object Factory {
        fun create(): MyClass = MyClass()
    }
}

val instance = MyClass.create()

Без явно заданного имени companion называется Companion. Его члены принадлежат экземпляру, хотя Kotlin разрешает обращаться к ним через имя класса. На JVM инициализация следует правилам статической инициализации внешнего класса; одна лишь загрузка класса не обязательно запускает её.

Для вызовов из Java @JvmStatic предоставляет статические методы, @JvmField открывает подходящие свойства как поля, а const val объявляет константы времени компиляции. Эти механизмы не взаимозаменяемы.

Специальные типы

inline class / value class

Value class вводит отдельный тип вокруг одного значения. На JVM используется @JvmInline:

@JvmInline
value class UserId(val value: String)

В отличие от псевдонима типа, UserId не взаимозаменяем со String. Это защищает от случайного смешивания доменных идентификаторов.

В первичном конструкторе value class должно быть одно read-only-свойство. Допустимы методы, вычисляемые свойства и проверки в init, но не дополнительные поля хранения. Он может реализовывать интерфейсы, не может наследовать другой класс и является final.

Компилятор может использовать исходное значение без обёртки, но при работе с обобщениями, интерфейсами и nullable-типами возможен boxing. Нельзя обещать отсутствие выделений памяти во всех случаях. Value class не имеет ссылочной идентичности; === запрещён. Для публичных API также важны преобразование имён функций на JVM и ограничения вызова из Java.

Связанные темы

Источники