277 lines
12 KiB
Markdown
277 lines
12 KiB
Markdown
---
|
||
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<Int>)
|
||
>
|
||
> 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)
|