--- title: Data class description: Автоматические equals, hashCode, toString, copy и componentN, их границы и ловушки tags: - kotlin - oop --- `data class` - класс, для которого компилятор генерирует стандартный набор методов. ```kotlin data class User(val id: Int, val name: String) fun main() { val u = User(1, "Иван") println(u) // User(id=1, name=Иван) println(u == User(1, "Иван")) // true println(u.copy(name = "Пётр")) // User(id=1, name=Пётр) val (id, name) = u println("$id $name") // 1 Иван } ``` Генерируется: | Метод | Что делает | |---|---| | `equals` | сравнивает все свойства основного конструктора | | `hashCode` | считает по тем же свойствам | | `toString` | `Имя(свойство=значение, ...)` | | `componentN` | для [деструктуризации](../Работа%20с%20данными/Деструктуризация.md) | | `copy` | копия с заменой части свойств | ## Требования ```kotlin data class Point(val x: Int, val y: Int) ``` - основной конструктор с **хотя бы одним** параметром - все параметры конструктора - `val` или `var` - класс не может быть `abstract`, `open`, `sealed` или `inner` ```kotlin // data class Empty // ошибка: нет параметров // data class Bad(x: Int) // ошибка: x не val/var ``` ## Только свойства конструктора Это главное, что нужно понимать про `data class`. Свойства, объявленные в теле, **не участвуют** в генерации: ```kotlin data class User(val id: Int) { var nickname: String = "" // в теле — игнорируется } fun main() { val a = User(1).apply { nickname = "иван" } val b = User(1).apply { nickname = "пётр" } println(a == b) // true! nickname не сравнивается println(a) // User(id=1) — nickname не печатается println(a.copy()) // User(id=1) — nickname потерян, станет "" } ``` Если свойство важно для равенства - оно должно быть в конструкторе. ## copy Создаёт новый объект, меняя указанные свойства: ```kotlin data class User(val id: Int, val name: String, val active: Boolean = true) fun main() { val u = User(1, "Иван") println(u.copy()) // User(id=1, name=Иван, active=true) println(u.copy(name = "Пётр")) // User(id=1, name=Пётр, active=true) println(u.copy(id = 2, active = false)) println(u) // исходный не изменился } ``` `copy` - основной способ «изменить» неизменяемый объект. Аргументы почти всегда именованные. ## Вложенные data class ```kotlin data class Address(val city: String, val street: String) data class Person(val name: String, val address: Address) fun main() { val p = Person("Иван", Address("Москва", "Арбат")) println(p == Person("Иван", Address("Москва", "Арбат"))) // true // замена вложенного свойства — copy внутри copy val moved = p.copy(address = p.address.copy(city = "Казань")) println(moved) // Person(name=Иван, address=Address(city=Казань, street=Арбат)) } ``` Для глубоких структур такие цепочки становятся громоздкими - это известная цена иммутабельности. ## Переопределение сгенерированного Любой из методов можно написать руками - свой вариант побеждает: ```kotlin data class User(val id: Int, val name: String) { override fun toString() = "#$id $name" } fun main() { println(User(1, "Иван")) // #1 Иван println(User(1, "Иван") == User(1, "Иван")) // true — equals сгенерирован } ``` Частый случай - равенство только по идентификатору: ```kotlin data class User(val id: Int, val name: String) { override fun equals(other: Any?) = other is User && other.id == id override fun hashCode() = id } fun main() { println(User(1, "Иван") == User(1, "Пётр")) // true — сравниваем id } ``` Но если равенство нестандартное, `data class` уже почти не нужен - остаются только `copy` и `toString`. ## В коллекциях Корректный `equals`/`hashCode` делает `data class` пригодным для `Set` и ключей `Map`: ```kotlin data class Key(val a: Int, val b: String) fun main() { val map = mapOf(Key(1, "x") to "значение") println(map[Key(1, "x")]) // значение — новый объект находит запись println(setOf(Key(1, "x"), Key(1, "x")).size) // 1 — дубликат убран println(listOf(Key(1, "x")).contains(Key(1, "x"))) // true } ``` ## Наследование `data class` не может быть `open`, но может реализовывать интерфейсы и наследоваться от обычного класса: ```kotlin interface Named { val name: String } data class User(override val name: String, val id: Int) : Named fun main() { val n: Named = User("Иван", 1) println(n.name) // Иван } ``` Часто `data class` реализуют варианты [sealed-иерархии](Sealed%20class%20и%20interface.md): ```kotlin sealed interface Event data class Click(val x: Int, val y: Int) : Event data class KeyPress(val code: Int) : Event data object Close : Event fun handle(e: Event): String = when (e) { is Click -> "клик ${e.x},${e.y}" is KeyPress -> "клавиша ${e.code}" Close -> "закрытие" } fun main() { println(handle(Click(1, 2))) // клик 1,2 println(handle(Close)) // закрытие } ``` ## data object Для варианта без данных есть `data object` (Kotlin 1.9+) - он отличается от обычного `object` осмысленным `toString`: ```kotlin object Plain data object Loading fun main() { println(Plain) // Plain@1b6d3586 println(Loading) // Loading } ``` Подробнее: [Object и companion object](Object%20и%20companion%20object.md) ## Когда не нужен - **Сущность с идентичностью** (entity в БД): равенство по всем полям неверно, нужно по `id` - **Класс с поведением**: если методов больше, чем свойств, это не носитель данных - **Публичный API, который будет развиваться**: порядок свойств в конструкторе становится контрактом из-за `componentN` > [!info] Отличия от Java > Ближайший аналог - `record` из Java 16: > ```java > record User(int id, String name) {} > ``` > > | | Java record | Kotlin data class | > |---|---|---| > | `equals`/`hashCode`/`toString` | да | да | > | копия с изменением | нет | `copy()` | > | деструктуризация | только в pattern matching | везде | > | изменяемые поля | нет, всегда final | `var` разрешён | > | наследование от класса | нет | можно | > | значения по умолчанию | нет | есть | > | своё тело | можно | можно | > > Главное практическое различие - `copy()`. В Java для изменения одного поля record приходится вызывать конструктор со всеми аргументами: > ```java > var moved = new User(u.id(), "Пётр"); > ``` > > Второе - `record` всегда неизменяем, `data class` допускает `var`. Это гибкость, которой лучше не пользоваться. > > До появления `record` в Java такие классы писали руками или генерировали Lombok-ом (`@Data`, `@Value`). Lombok генерировал ещё и `@Builder`, аналога которому в Kotlin нет - его заменяют значения по умолчанию и именованные аргументы. > > Сравнить: [Record в Java](../../../Программирование%20на%20Java/Синтаксис%20и%20типы/Базовые%20типы%20ООП/Record.md) > [!warning] Подводные камни > **Массив в `data class` ломает равенство:** > ```kotlin > data class Packet(val data: ByteArray) > > fun main() { > println(Packet(byteArrayOf(1)) == Packet(byteArrayOf(1))) // false! > } > ``` > Сгенерированный `equals` вызывает `data.equals(other.data)`, а у массива это сравнение ссылок. Нужно писать руками: > ```kotlin > class Packet(val data: ByteArray) { > override fun equals(other: Any?) = > other is Packet && data.contentEquals(other.data) > override fun hashCode() = data.contentHashCode() > } > > fun main() { > println(Packet(byteArrayOf(1)) == Packet(byteArrayOf(1))) // true > } > ``` > Подробнее: [Массивы](../Работа%20с%20данными/Массивы.md) > > **`var` в `data class` + использование как ключа `Map` = потерянная запись:** > ```kotlin > data class Key(var id: Int) > > fun main() { > val key = Key(1) > val map = hashMapOf(key to "значение") > key.id = 2 // hashCode изменился > println(map[key]) // null — запись потеряна > println(map[Key(1)]) // null — старый бакет, другой equals > println(map.size) // 1 — но она там есть > } > ``` > Это худшая ошибка с `data class`. Поэтому свойства делают `val`. > > **`copy` копирует поверхностно.** Вложенный изменяемый объект остаётся общим: > ```kotlin > data class Box(val items: MutableList) > > fun main() { > val a = Box(mutableListOf(1)) > val b = a.copy() > b.items.add(2) > println(a.items) // [1, 2] — изменился и оригинал! > } > ``` > > **Свойства в теле класса не входят ни в `equals`, ни в `copy`, ни в `toString`** - значение теряется при копировании. > > **Порядок свойств - часть API** из-за `componentN`. Перестановка свойств одинакового типа не вызовет ошибки компиляции у клиентов, но сломает их деструктуризацию. Подробнее: [Деструктуризация](../Работа%20с%20данными/Деструктуризация.md) > > **`copy` до Kotlin 2.0 игнорировал видимость конструктора.** Приватный конструктор не защищал от создания невалидного объекта через `copy`. С Kotlin 2.1 `copy` получает ту же видимость, что конструктор, если включён `-Xconsistent-data-class-copy-visibility`; это станет поведением по умолчанию. > > **`toString` печатает всё, включая пароли и токены.** Логирование `data class` с секретами - реальная утечка: > ```kotlin > data class Credentials(val login: String, val password: String) { > override fun toString() = "Credentials(login=$login, password=***)" > } > > fun main() { println(Credentials("ivan", "secret")) } > ``` Дополнительно: [equals, hashCode и toString](equals,%20hashCode%20и%20toString.md) Далее: [Enum class](Enum%20class.md)