Kotlin guide done. (20.10.26)

This commit is contained in:
2026-10-20 12:54:25 +03:00
parent da3ad9dd1a
commit 6853ada348
81 changed files with 30112 additions and 0 deletions
@@ -0,0 +1,276 @@
---
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)