Classes & Types
Классы, объявления объектов и типы-значения Kotlin используются в Android для моделей данных, состояния UI и API. Выбирать тип стоит по его смыслу, а не только по количеству сэкономленного шаблонного кода.
Классы и модели
data class
Для data class обычно генерируются equals(), hashCode(), toString(), copy() и componentN() на основе свойств первичного конструктора. Собственные реализации первых трёх функций или унаследованные final-реализации могут заменять их генерацию.
В первичном конструкторе нужен хотя бы один параметр, причём все его параметры должны быть val или var.
Свойства в теле класса не участвуют в этих сгенерированных функциях. 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, инициализируемый при первом обращении. Инициализация потокобезопасна, но последующие операции с изменяемым состоянием не синхронизируются автоматически.
На 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:
В отличие от псевдонима типа, UserId не взаимозаменяем со String. Это защищает от случайного смешивания доменных идентификаторов.
В первичном конструкторе value class должно быть одно read-only-свойство. Допустимы методы, вычисляемые свойства и проверки в init, но не дополнительные поля хранения. Он может реализовывать интерфейсы, не может наследовать другой класс и является final.
Компилятор может использовать исходное значение без обёртки, но при работе с обобщениями, интерфейсами и nullable-типами возможен boxing. Нельзя обещать отсутствие выделений памяти во всех случаях. Value class не имеет ссылочной идентичности; === запрещён. Для публичных API также важны преобразование имён функций на JVM и ограничения вызова из Java.