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,274 @@
---
title: Any, Unit, Nothing
description: Корень иерархии Any, тип-единица Unit и нижний тип Nothing
tags:
- kotlin
- types
---
Три особых типа образуют каркас системы типов Kotlin.
| Тип | Сколько значений | Роль |
|---|---|---|
| `Any` | все объекты | супертип всего |
| `Unit` | одно (`Unit`) | «результата нет» |
| `Nothing` | ни одного | «возврата не будет» |
```mermaid
graph TD
A["Any?"] --> B["Any"]
A --> N1["Nothing?"]
B --> C["String"]
B --> D["Int"]
B --> E["Unit"]
C --> N["Nothing"]
D --> N
E --> N
N1 --> N
```
`Any` сверху, `Nothing` снизу: он подтип любого типа.
## Any
Супертип всех non-null типов. Объявляет три метода:
```kotlin
fun main() {
val x: Any = 42
println(x.toString()) // 42
println(x.hashCode()) // 42
println(x == 42) // true — вызов equals
}
```
Больше у `Any` ничего нет: ни `wait`, ни `notify`, ни `getClass`.
```kotlin
fun main() {
val items: List<Any> = listOf(1, "два", 3.0, true)
println(items.joinToString { it::class.simpleName ?: "?" })
// Int, String, Double, Boolean
}
```
`Any` не принимает `null` - для этого есть `Any?`, самый широкий тип в языке:
```kotlin
fun main() {
// val a: Any = null // ошибка компиляции
val b: Any? = null // ok
println(b) // null
}
```
Расширения `Any?` можно звать на `null`:
```kotlin
fun main() {
val x: String? = null
println(x.toString()) // "null" — расширение Any?.toString()
println(x.hashCode()) // 0
}
```
Это не методы `Any`, а функции-расширения stdlib, объявленные для `Any?`.
## Unit
Тип функций, которые ничего не возвращают. Это **настоящий объект**, а не отсутствие значения:
```kotlin
fun log(msg: String): Unit = println(msg)
fun main() {
val result = log("привет")
println(result) // kotlin.Unit
println(result == Unit) // true
}
```
`Unit` - `object`, у него ровно одно значение. Тип результата `Unit` можно опускать, и обычно так и делают:
```kotlin
fun log(msg: String) = println(msg) // тип выведен: Unit
fun log2(msg: String) { println(msg) } // то же самое
```
Поскольку `Unit` - полноценный тип, он подставляется в дженерики, где `void` в Java не работает:
```kotlin
fun main() {
val actions: List<() -> Unit> = listOf({ print("a") }, { print("b") })
actions.forEach { it() } // ab
println()
val map = mutableMapOf<String, Unit>()
map["ключ"] = Unit // Unit как значение — законно
println(map) // {ключ=kotlin.Unit}
}
```
В лямбде с типом результата `Unit` последнее выражение игнорируется - возвращать ничего не нужно:
```kotlin
fun run2(block: () -> Unit) = block()
fun main() {
run2 { 42 } // 42 отбрасывается, лямбда подходит под () -> Unit
println("ok")
}
```
## Nothing
Тип без значений. Функция с результатом `Nothing` **не может завершиться нормально** - только исключением или бесконечным циклом:
```kotlin
fun fail(message: String): Nothing = throw IllegalStateException(message)
fun main() {
val x: String = if (true) "ok" else fail("нет значения")
println(x) // ok
}
```
Зачем это нужно: `Nothing` - подтип любого типа, поэтому выражение с типом `Nothing` подходит в любую позицию. Компилятор знает, что дальше кода не будет.
```kotlin
fun fail(msg: String): Nothing = throw IllegalArgumentException(msg)
fun parse(s: String): Int = s.toIntOrNull() ?: fail("не число: $s")
fun main() {
println(parse("42")) // 42
// parse("abc") // IllegalArgumentException: не число: abc
}
```
Если бы `fail` возвращала `Unit`, этот код не скомпилировался бы: `Int` и `Unit` несовместимы.
`throw` сам по себе - выражение типа `Nothing`:
```kotlin
fun main() {
val x: Int = try {
"abc".toInt()
} catch (e: NumberFormatException) {
throw IllegalStateException("плохие данные", e) // Nothing подходит
}
println(x)
}
```
### Анализ недостижимого кода
```kotlin
fun fail(): Nothing = throw RuntimeException()
fun main() {
val x = 1
if (x > 0) {
println("да")
} else {
fail()
// println("недостижимо") // предупреждение компилятора
}
}
```
Компилятор использует `Nothing`, чтобы доказать: ветка не продолжается. Поэтому `TODO()` работает где угодно:
```kotlin
fun calculate(x: Int): String = TODO("ещё не реализовано")
fun main() {
// calculate(1) // NotImplementedError
println("компилируется")
}
```
`TODO()` объявлена как `fun TODO(): Nothing`, поэтому подходит под любой тип результата.
### Nothing? и null
```kotlin
fun main() {
val x = null
println(x) // null
// тип x — Nothing?, единственное значение которого null
val list = listOf(null, null) // List<Nothing?>
println(list.size) // 2
}
```
Пустые коллекции тоже используют `Nothing`:
```kotlin
fun main() {
val empty: List<Nothing> = emptyList()
val strings: List<String> = empty // подходит — List ковариантен
println(strings.size) // 0
}
```
Один объект `EmptyList` подходит под `List<T>` для любого `T` именно потому, что `List<Nothing>` - подтип всех `List<T>`.
> [!info] Отличия от Java
> | Java | Kotlin |
> |---|---|
> | `Object` | `Any` (но без `wait`/`notify`/`getClass`) |
> | `void` | `Unit` |
> | `Void` | `Unit` (или `Nothing?`) |
> | нет аналога | `Nothing` |
> | `Object o = null;` | `Any? = null`, `Any` не принимает null |
>
> **`Any` беднее `Object`.** Методы `wait()`, `notify()`, `getClass()` в `Any` не объявлены: первые два относятся к устаревшей модели синхронизации, третий заменён на `::class`. На JVM они доступны после приведения к `Object`, но из Kotlin их намеренно не видно.
>
> **`Unit` против `void`** - не косметика. `void` не тип, поэтому в Java нельзя написать `List<void>` или `Function<String, void>`; приходится использовать `Void` (у которого единственное значение - `null`) или `Consumer`. В Kotlin `() -> Unit` - обычный тип.
>
> **`Nothing` в Java нет вообще.** Из-за этого Java-компилятор не знает, что метод никогда не вернёт управление:
> ```java
> int parse(String s) {
> if (s == null) fail("null"); // fail объявлен void
> return Integer.parseInt(s); // компилятор требует этот return
> }
> ```
> В Kotlin `fail(): Nothing` сообщает компилятору, что ветка обрывается, и анализ потока это учитывает.
>
> Сравнить: [Класс Object в Java](../../../Программирование%20на%20Java/Синтаксис%20и%20типы/Базовые%20типы%20ООП/Класс%20Object.md)
> [!warning] Подводные камни
> **`Any` - не то же самое, что `Any?`.** Функция, принимающая `Any`, не примет `null`:
> ```kotlin
> fun show(x: Any) = x.toString()
>
> fun main() {
> val n: String? = null
> // println(show(n)) // ошибка компиляции
> println(show(n ?: "пусто")) // пусто
> }
> ```
>
> **Лямбда с результатом `Unit` молча глотает значение:**
> ```kotlin
> fun main() {
> val items = mutableListOf<Int>()
> listOf(1, 2).forEach { items.add(it) } // add возвращает Boolean,
> println(items) // но тип () -> Unit его отбрасывает
> }
> ```
> Это обычно удобно, но если по ошибке вместо `map` написать `forEach`, результат исчезнет без предупреждения.
>
> **`return` с типом `Unit` писать не нужно**, а `return Unit` - признак непонимания:
> ```kotlin
> fun f(): Unit {
> return Unit // компилируется, но так не пишут
> }
> ```
>
> **Функция с типом `Nothing` не может иметь тело-блок с `return`:**
> ```kotlin
> fun fail(): Nothing {
> // return // ошибка: ничего нельзя вернуть
> throw RuntimeException() // только выход через исключение
> }
> ```
>
> **`val x = null` имеет тип `Nothing?`, а не `Any?`.** Присвоить такой переменной что-либо другое нельзя:
> ```kotlin
> fun main() {
> var x = null
> // x = "строка" // ошибка: ожидался Nothing?
> val y: String? = null // так
> println("$x $y")
> }
> ```
>
> **`Nothing` в дженерике означает «значений не будет»:**
> ```kotlin
> fun main() {
> val producer: () -> Nothing = { throw RuntimeException("всегда падает") }
> // producer() — вызов гарантированно бросит исключение
> println("тип ${producer::class.simpleName} объявлен, но вызов невозможен")
> }
> ```
> Если такой тип получился случайно (например, `emptyList()` без указания параметра), в коллекцию ничего нельзя добавить.
Дополнительно: [equals, hashCode и toString](equals,%20hashCode%20и%20toString.md)
Далее: [Data class](Data%20class.md)
@@ -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)
@@ -0,0 +1,298 @@
---
title: Enum class
description: Перечисления со свойствами и методами, entries, when по enum и его границы
tags:
- kotlin
- oop
---
`enum class` - класс с фиксированным набором экземпляров.
```kotlin
enum class Status { ACTIVE, BLOCKED, DELETED }
fun main() {
val s = Status.ACTIVE
println(s) // ACTIVE
println(s.name) // ACTIVE — строка
println(s.ordinal) // 0 — позиция в объявлении
}
```
## Свойства и конструктор
Каждая константа - вызов конструктора:
```kotlin
enum class Planet(val radiusKm: Int, val hasRings: Boolean) {
EARTH(6371, false),
SATURN(58232, true),
MARS(3390, false);
val radiusM: Int get() = radiusKm * 1000
fun describe() = "$name: радиус $radiusKm км, кольца=$hasRings"
}
fun main() {
println(Planet.SATURN.describe()) // SATURN: радиус 58232 км, кольца=true
println(Planet.EARTH.radiusM) // 6371000
}
```
Точка с запятой после последней константы обязательна, если дальше идёт тело класса.
## Методы в константах
Константа может переопределить метод - это заменяет `when` по самой себе:
```kotlin
enum class Operation {
PLUS { override fun apply(a: Int, b: Int) = a + b },
MINUS { override fun apply(a: Int, b: Int) = a - b },
TIMES { override fun apply(a: Int, b: Int) = a * b };
abstract fun apply(a: Int, b: Int): Int
}
fun main() {
println(Operation.PLUS.apply(2, 3)) // 5
println(Operation.TIMES.apply(2, 3)) // 6
println(Operation.entries.joinToString { "${it.name}=${it.apply(6, 3)}" })
// PLUS=9, MINUS=3, TIMES=18
}
```
Каждая такая константа компилируется в анонимный подкласс.
## Реализация интерфейса
```kotlin
interface Describable { fun describe(): String }
enum class Level(val weight: Int) : Describable {
LOW(1), HIGH(10);
override fun describe() = "$name (вес $weight)"
}
fun main() {
val items: List<Describable> = Level.entries
println(items.joinToString { it.describe() }) // LOW (вес 1), HIGH (вес 10)
}
```
## entries, values, valueOf
```kotlin
enum class Status { ACTIVE, BLOCKED }
fun main() {
println(Status.entries) // [ACTIVE, BLOCKED] — список
println(Status.entries.size) // 2
println(Status.valueOf("ACTIVE")) // ACTIVE
println(Status.entries.first().name) // ACTIVE
// println(Status.valueOf("НЕТ")) // IllegalArgumentException
}
```
`entries` (Kotlin 1.9+) заменяет `values()`. Разница практическая:
| | `values()` | `entries` |
|---|---|---|
| тип | `Array<Status>` | `EnumEntries<Status>` (это `List`) |
| аллокация | **новый массив при каждом вызове** | один неизменяемый список |
| изменяемость | элементы можно подменить | нельзя |
```kotlin
enum class Status { ACTIVE, BLOCKED }
fun main() {
@Suppress("DEPRECATION")
println(Status.values().contentToString()) // [ACTIVE, BLOCKED]
println(Status.entries) // [ACTIVE, BLOCKED]
}
```
В новом коде всегда `entries`.
Безопасный разбор строки:
```kotlin
enum class Status { ACTIVE, BLOCKED }
fun parseStatus(s: String): Status? =
Status.entries.find { it.name == s }
fun main() {
println(parseStatus("ACTIVE")) // ACTIVE
println(parseStatus("нет")) // null — без исключения
}
```
Через `enumValueOf` то же самое, но с исключением. Для внешних данных (JSON, БД, параметры запроса) нужна версия с `null`.
## when по enum
Главное применение. Компилятор проверяет полноту, и `else` не нужен:
```kotlin
enum class Status { ACTIVE, BLOCKED, DELETED }
fun message(s: Status): String = when (s) {
Status.ACTIVE -> "работает"
Status.BLOCKED -> "заблокирован"
Status.DELETED -> "удалён"
}
fun main() {
println(message(Status.BLOCKED)) // заблокирован
}
```
Если добавить константу `ARCHIVED`, этот `when` перестанет компилироваться - компилятор найдёт все места, которые нужно обновить. Поэтому `else` в `when` по enum - вредная привычка: он скрывает именно те ошибки, от которых enum защищает.
Импорт констант сокращает запись:
```kotlin
enum class Status { ACTIVE, BLOCKED }
fun message(s: Status): String {
return when (s) {
Status.ACTIVE -> "работает"
Status.BLOCKED -> "заблокирован"
}
}
fun main() = println(message(Status.ACTIVE))
```
## Сравнение и порядок
```kotlin
enum class Level { LOW, MEDIUM, HIGH }
fun main() {
println(Level.LOW == Level.LOW) // true
println(Level.LOW < Level.HIGH) // true — по ordinal
println(Level.entries.sorted()) // [LOW, MEDIUM, HIGH]
println(Level.HIGH.ordinal) // 2
println(Level.entries.maxOf { it.ordinal }) // 2
}
```
`enum` реализует `Comparable` по `ordinal`. Сравнивать через `==` безопасно: экземпляр единственный, поэтому `==` и `===` дают одно и то же.
## EnumMap и EnumSet
Специализированные коллекции для enum - массив вместо хеш-таблицы:
```kotlin
import java.util.EnumMap
import java.util.EnumSet
enum class Day { MON, TUE, WED }
fun main() {
val hours = EnumMap<Day, Int>(Day::class.java)
hours[Day.MON] = 8
hours[Day.TUE] = 6
println(hours) // {MON=8, TUE=6}
val workdays = EnumSet.of(Day.MON, Day.TUE)
println(workdays.contains(Day.WED)) // false
println(EnumSet.complementOf(workdays)) // [WED]
}
```
Это классы JDK; в stdlib Kotlin аналогов нет. Для небольших enum разница с `HashMap` несущественна.
## companion object
```kotlin
enum class Status(val code: Int) {
ACTIVE(1), BLOCKED(2);
companion object {
private val byCode = entries.associateBy { it.code }
fun fromCode(code: Int): Status? = byCode[code]
}
}
fun main() {
println(Status.fromCode(2)) // BLOCKED
println(Status.fromCode(99)) // null
}
```
Поиск по коду через заранее построенную `Map` - стандартный приём. Подробнее: [Object и companion object](Object%20и%20companion%20object.md)
## Когда enum, а когда sealed
```kotlin
enum class Status { OK, ERROR } // варианты без данных
sealed interface Result2
data class Ok(val value: Int) : Result2 // варианты с разными данными
data class Error(val message: String) : Result2
```
`enum` - фиксированный набор **значений**, у всех одинаковая структура. `sealed` - фиксированный набор **типов**, у каждого свои свойства. Подробнее: [Sealed class и interface](Sealed%20class%20и%20interface.md)
> [!info] Отличия от Java
> | Java | Kotlin |
> |---|---|
> | `enum Status { ACTIVE }` | `enum class Status { ACTIVE }` |
> | `Status.values()` | `Status.entries` |
> | `Status.valueOf(s)` | `Status.valueOf(s)` или `entries.find` |
> | `switch` без проверки полноты до Java 21 | `when` проверяет всегда |
> | `EnumMap`/`EnumSet` | то же (из JDK) |
>
> Устройство одинаковое - на JVM это тот же `java.lang.Enum`. Два практических различия:
>
> **`entries` вместо `values()`.** Java-метод `values()` возвращает новый массив при каждом вызове, потому что массив изменяем и вернуть общий нельзя. `entries` возвращает неизменяемый список, поэтому экземпляр один. В цикле это заметная разница.
>
> **Проверка полноты.** В Java `switch` по enum без `default` долго не проверялся: добавление константы молча менялo поведение. Kotlin проверяет `when`-выражение с момента появления языка, а с Kotlin 2.0 - и `when`-инструкцию.
>
> Сравнить: [Enum в Java](../../../Программирование%20на%20Java/Синтаксис%20и%20типы/Базовые%20типы%20ООП/Enum.md)
> [!warning] Подводные камни
> **`ordinal` нельзя сохранять в БД или в файл.** Он меняется при вставке константы в середину:
> ```kotlin
> enum class Status { ACTIVE, BLOCKED } // BLOCKED.ordinal == 1
> // добавили NEW в середину:
> // enum class Status { ACTIVE, NEW, BLOCKED } // BLOCKED.ordinal == 2
> ```
> Все сохранённые единицы станут означать `NEW`. Для сериализации используйте `name` или свой стабильный `code`.
>
> **`name` тоже хрупок при переименовании** - но его хотя бы нельзя изменить случайно. Самый надёжный вариант - явное свойство:
> ```kotlin
> enum class Status(val code: String) {
> ACTIVE("active"), BLOCKED("blocked")
> }
>
> fun main() { println(Status.ACTIVE.code) } // active
> ```
>
> **`valueOf` бросает исключение на незнакомой строке**, а не возвращает `null`. Для внешних данных нужна своя обёртка через `entries.find`.
>
> **`values()` аллоцирует массив на каждый вызов:**
> ```kotlin
> enum class Status { ACTIVE }
>
> fun main() {
> @Suppress("DEPRECATION")
> repeat(3) { Status.values() } // три разных массива
> repeat(3) { Status.entries } // один и тот же список
> println("ok")
> }
> ```
>
> **`else` в `when` по enum отключает проверку полноты.** Самая частая ошибка: добавили константу, компилятор промолчал, в рантайме сработала ветка `else`.
>
> **Enum нельзя наследовать и расширять.** Если набор вариантов должен быть расширяемым (например, плагинами), enum не подходит - нужен `sealed interface` или обычная иерархия.
>
> **Enum с изменяемым состоянием - глобальная переменная:**
> ```kotlin
> enum class Counter {
> INSTANCE;
> var count = 0 // общее состояние на всю программу
> }
>
> fun main() {
> Counter.INSTANCE.count++
> println(Counter.INSTANCE.count) // 1 — и так во всех потоках
> }
> ```
> Константы enum - синглтоны, поэтому `var` в них небезопасен при многопоточности.
>
> **`when` по nullable enum всё равно требует `else` или ветки `null`:**
> ```kotlin
> enum class Status { ACTIVE, BLOCKED }
>
> fun message(s: Status?): String = when (s) {
> Status.ACTIVE -> "работает"
> Status.BLOCKED -> "заблокирован"
> null -> "не задан"
> }
>
> fun main() { println(message(null)) } // не задан
> ```
Дополнительно: [Sealed class и interface](Sealed%20class%20и%20interface.md)
Далее: [Sealed class и interface](Sealed%20class%20и%20interface.md)
@@ -0,0 +1,347 @@
---
title: Object и companion object
description: Синглтон через object, объект-выражение, companion object вместо static и фабрики
tags:
- kotlin
- oop
---
Слово `object` в Kotlin означает три разные вещи: объявление синглтона, выражение-анонимный объект и компаньон класса.
## object - синглтон
```kotlin
object Config {
val appName = "MyApp"
var debug = false
fun describe() = "$appName debug=$debug"
}
fun main() {
println(Config.appName) // MyApp
Config.debug = true
println(Config.describe()) // MyApp debug=true
}
```
Экземпляр создаётся **один** и **лениво** - при первом обращении. Потокобезопасность обеспечивает JVM через механизм инициализации классов.
```kotlin
object Lazy2 {
init { println("инициализация") }
val value = 42
}
fun main() {
println("до обращения") // до обращения
println(Lazy2.value) // инициализация, затем 42
println(Lazy2.value) // 42 — повторной инициализации нет
}
```
Объект может наследоваться и реализовывать интерфейсы:
```kotlin
interface Logger { fun log(msg: String) }
object ConsoleLogger : Logger {
override fun log(msg: String) = println("[LOG] $msg")
}
fun main() {
val l: Logger = ConsoleLogger
l.log("привет") // [LOG] привет
}
```
Это главное отличие от набора статических методов: синглтон - полноценный объект, его можно передать туда, где ожидается интерфейс.
У `object` не может быть конструктора - передать ему параметры при создании невозможно.
## data object
Для случаев, где нужен осмысленный `toString` (обычно в [sealed-иерархиях](Sealed%20class%20и%20interface.md)):
```kotlin
object Plain
data object Loading
fun main() {
println(Plain) // Plain@1b6d3586
println(Loading) // Loading
println(Loading == Loading) // true
println(Loading.hashCode() == Loading.hashCode()) // true
}
```
`data object` генерирует `toString`, `equals` и `hashCode`. Для синглтона `equals` и так работал по ссылке - полезен именно `toString`.
## Объект-выражение
Анонимный объект создаётся на месте:
```kotlin
interface ClickListener {
fun onClick(x: Int, y: Int)
}
fun main() {
val listener = object : ClickListener {
override fun onClick(x: Int, y: Int) = println("клик $x,$y")
}
listener.onClick(1, 2) // клик 1,2
}
```
В отличие от объявления `object`, выражение создаёт **новый** экземпляр при каждом вычислении:
```kotlin
private fun makeCounter() = object {
var count = 0
fun inc() = ++count
}
fun main() {
val a = makeCounter()
val b = makeCounter()
a.inc(); a.inc()
println("${a.inc()} ${b.inc()}") // 3 1 — независимые объекты
}
```
Можно реализовать несколько интерфейсов сразу и даже ни одного:
```kotlin
fun main() {
val point = object {
val x = 1
val y = 2
override fun toString() = "($x, $y)"
}
println(point) // (1, 2)
println(point.x) // 1
}
```
Тип такого объекта анонимен, поэтому за пределы локальной области он не выходит:
```kotlin
class Holder {
private fun make() = object { val x = 1 } // private — тип виден
fun use() = make().x
// fun bad() = object { val x = 1 } // public: тип станет Any,
} // и x будет недоступен
fun main() { println(Holder().use()) } // 1
```
Объект-выражение видит и может менять локальные переменные:
```kotlin
fun main() {
var clicks = 0
val listener = object : Runnable {
override fun run() { clicks++ }
}
listener.run(); listener.run()
println(clicks) // 2
}
```
## companion object
Члены, привязанные к классу, а не к экземпляру:
```kotlin
class User private constructor(val name: String) {
companion object {
const val MAX_NAME_LENGTH = 50
fun create(name: String): User? =
if (name.length <= MAX_NAME_LENGTH) User(name) else null
}
}
fun main() {
println(User.MAX_NAME_LENGTH) // 50
println(User.create("Иван")?.name) // Иван
println(User.create("x".repeat(99))) // null
}
```
Обращение идёт через имя класса - как к статическим членам. Приватный конструктор + фабрика в компаньоне - стандартный способ контролировать создание объектов.
Компаньон может быть только один, и у него может быть имя:
```kotlin
class User {
companion object Factory {
fun create() = User()
}
}
fun main() {
println(User.create()::class.simpleName) // User
println(User.Factory.create()::class.simpleName) // User — через имя
}
```
Имя нужно редко - в основном для вызова из Java.
### Компаньон - это объект
Поэтому он может реализовывать интерфейсы:
```kotlin
interface Parser<T> {
fun parse(s: String): T?
}
class Color2(val rgb: Int) {
companion object : Parser<Color2> {
override fun parse(s: String): Color2? =
s.removePrefix("#").toIntOrNull(16)?.let { Color2(it) }
}
}
fun main() {
println(Color2.parse("#ff0000")?.rgb) // 16711680
val p: Parser<Color2> = Color2 // компаньон как значение
println(p.parse("#00ff00")?.rgb) // 65280
}
```
Статические методы в Java так передать нельзя - это реальное преимущество.
### Расширения компаньона
```kotlin
class User(val name: String) {
companion object
}
fun User.Companion.anonymous() = User("анонимный")
fun main() {
println(User.anonymous().name) // анонимный
}
```
Пустой `companion object` объявляют именно для этого - чтобы к классу можно было добавить «статическую» фабрику извне.
## const и @JvmStatic
```kotlin
class Config {
companion object {
const val VERSION = "1.0" // константа компиляции
val runtime = System.nanoTime() // вычисляется при инициализации
@JvmStatic
fun helper() = "вызывается из Java как Config.helper()"
}
}
fun main() {
println(Config.VERSION)
println(Config.helper())
}
```
`const` работает только для примитивов и `String`, и только на верхнем уровне, в `object` или в `companion object`. Значение подставляется в место использования при компиляции.
`@JvmStatic` делает член настоящим статическим в байт-коде. Без неё Java-код обращается через `Config.Companion.helper()`. Подробнее: [Kotlin из Java](../../Взаимодействие%20с%20Java/Kotlin%20из%20Java.md)
## object или функции верхнего уровня
```kotlin
// вариант 1: функции файла
fun formatDate(d: Long) = "дата $d"
// вариант 2: object
object DateUtils {
fun format(d: Long) = "дата $d"
}
```
Если состояния нет и интерфейс реализовывать не нужно - функции верхнего уровня проще: не нужно писать префикс, и в байт-коде нет лишнего экземпляра. `object` оправдан, когда есть состояние или нужна реализация интерфейса.
## Три формы рядом
| Форма | Экземпляров | Когда |
|---|---|---|
| `object Name { }` | один, ленивый | синглтон: конфиг, реестр, реализация интерфейса |
| `object : Type { }` | новый каждый раз | колбэк, одноразовая реализация |
| `companion object` | один на класс | фабрики, константы, «статика» |
> [!info] Отличия от Java
> | Java | Kotlin |
> |---|---|
> | `static` поля и методы | `companion object` или функции файла |
> | `static final` константа | `const val` |
> | паттерн Singleton (enum/holder) | `object` |
> | анонимный класс `new Iface() {}` | `object : Iface {}` |
> | `static` блок инициализации | `init` в `object`/`companion object` |
> | статику нельзя передать как объект | компаньон реализует интерфейсы |
> | лямбда только для SAM | `object` для любого числа методов |
>
> **Ключевое: в Kotlin нет `static`.** Это сделано намеренно - статика не участвует в полиморфизме и не реализует интерфейсы. Вместо неё либо функции верхнего уровня (для чистых утилит), либо `companion object` (когда член логически принадлежит классу).
>
> **Синглтон через `object` решает классическую задачу Java.** Корректный синглтон в Java требует либо `enum` с единственной константой, либо holder-класса с двойной проверкой блокировки:
> ```java
> class Config {
> private Config() {}
> private static class Holder { static final Config INSTANCE = new Config(); }
> static Config getInstance() { return Holder.INSTANCE; }
> }
> ```
> В Kotlin это `object Config { }` - с той же ленивостью и потокобезопасностью.
>
> **Анонимный класс Java не может реализовать два интерфейса**, объект-выражение Kotlin - может.
>
> Сравнить: [Singleton в Java](../../../Программирование%20на%20Java/Паттерны%20проектирования/Singleton.md), [Вложенные классы в Java](../../../Программирование%20на%20Java/ООП/Вложенные%20классы.md)
> [!warning] Подводные камни
> **`object` - глобальное изменяемое состояние, если в нём есть `var`:**
> ```kotlin
> object Counter {
> var count = 0 // общий на всю программу, не потокобезопасен
> }
>
> fun main() {
> Counter.count++
> println(Counter.count) // 1
> }
> ```
> Инкремент из нескольких потоков потеряет обновления. Нужен `AtomicInteger` или синхронизация. Подробнее: [Разделяемое состояние](../../Корутины/Разделяемое%20состояние.md)
>
> **Синглтон затрудняет тестирование.** Состояние `object` переживает тесты, и порядок их выполнения начинает влиять на результат. Для подменяемых зависимостей лучше внедрение через конструктор.
>
> **Инициализация `object` ленивая - и порядок может удивить:**
> ```kotlin
> object A {
> val value = B.value + 1
> }
> object B {
> val value = 10
> }
>
> fun main() { println(A.value) } // 11
> ```
> При круговой зависимости между объектами одно из значений окажется `null` или `0`, без всякой ошибки.
>
> **`const val` нельзя применить к сложному типу:**
> ```kotlin
> object Config {
> const val NAME = "app" // ok
> // const val LIST = listOf(1) // ошибка: только примитивы и String
> val LIST = listOf(1) // так
> }
>
> fun main() { println("${Config.NAME} ${Config.LIST}") }
> ```
>
> **Компаньон не наследуется.** У подкласса нет доступа к компаньону родителя как к своему:
> ```kotlin
> open class Base {
> companion object { fun create() = "из Base" }
> }
> class Child : Base()
>
> fun main() {
> println(Base.create()) // из Base
> // println(Child.create()) // ошибка: unresolved reference
> }
> ```
>
> **Тип объекта-выражения анонимен.** Если функция публичная, её тип результата схлопнется до супертипа (или `Any`), и дополнительные члены станут недоступны.
>
> **Из Java члены компаньона видны через `Companion`:**
> ```kotlin
> class Config {
> companion object {
> fun helper() = "без JvmStatic"
> @JvmStatic fun helper2() = "с JvmStatic"
> }
> }
>
> fun main() { println("${Config.helper()} / ${Config.helper2()}") }
> ```
> Java-код вызывает первую как `Config.Companion.helper()`, вторую - как `Config.helper2()`.
>
> **`object` внутри класса - не то же, что `companion object`.** Вложенный `object` требует обращения через своё имя: `Outer.Inner.member`.
Дополнительно: [Sealed class и interface](Sealed%20class%20и%20interface.md)
Далее: [Typealias](Typealias.md)
@@ -0,0 +1,294 @@
---
title: Sealed class и interface
description: Закрытые иерархии, исчерпывающий when, выбор между sealed class и interface
tags:
- kotlin
- oop
---
`sealed` означает: все наследники известны компилятору. Новый наследник может появиться только в том же модуле и пакете.
```kotlin
sealed interface Result2
data class Success(val data: String) : Result2
data class Failure(val error: Throwable) : Result2
data object Loading : Result2
fun render(r: Result2): String = when (r) {
is Success -> "данные: ${r.data}"
is Failure -> "ошибка: ${r.error.message}"
Loading -> "загрузка"
}
fun main() {
println(render(Success("ok"))) // данные: ok
println(render(Failure(RuntimeException("сбой")))) // ошибка: сбой
println(render(Loading)) // загрузка
}
```
`else` не нужен: компилятор видит все три варианта. Добавление четвёртого сломает компиляцию во всех `when` - это и есть цель.
## Зачем это нужно
`sealed` - способ выразить «одно из» в системе типов. Без него альтернативой был бы класс со всеми полями сразу и флагом:
```kotlin
// плохо: половина полей всегда null
class BadResult(val data: String?, val error: Throwable?, val loading: Boolean)
```
Такой класс позволяет создать бессмысленные состояния: данные и ошибка одновременно. `sealed` их запрещает на уровне типов.
## sealed class или sealed interface
```kotlin
sealed class Shape {
abstract val area: Double // общее состояние и поведение
}
class Circle(val r: Double) : Shape() {
override val area get() = Math.PI * r * r
}
class Square(val side: Double) : Shape() {
override val area get() = side * side
}
fun main() {
val shapes: List<Shape> = listOf(Circle(1.0), Square(2.0))
println(shapes.sumOf { it.area }) // 7.141592653589793
}
```
| | `sealed class` | `sealed interface` |
|---|---|---|
| состояние (свойства с полем) | можно | нет |
| конструктор | есть | нет |
| наследник может входить в другую иерархию | нет (один супертип-класс) | да |
| `data class` как наследник | да | да |
Правило: берите `sealed interface`, если общего состояния нет. Он гибче - один тип может реализовать несколько sealed-интерфейсов:
```kotlin
sealed interface Error2
sealed interface Retryable
data class Timeout(val seconds: Int) : Error2, Retryable
data class NotFound(val id: Int) : Error2
fun describe(e: Error2) = when (e) {
is Timeout -> "таймаут, можно повторить: ${e is Retryable}"
is NotFound -> "не найдено ${e.id}"
}
fun main() {
println(describe(Timeout(30))) // таймаут, можно повторить: true
println(describe(NotFound(1))) // не найдено 1
}
```
## Где можно объявлять наследников
С Kotlin 1.5 наследники должны быть в том же **модуле** и **пакете**, но не обязательно в том же файле:
```kotlin
// файл Result.kt
sealed interface Result2
// файл Success.kt — тот же пакет, тот же модуль
// data class Success(val data: String) : Result2
```
Раньше требовался один файл. Сейчас обычно всё равно держат в одном - иерархию видно целиком.
Вложенное объявление - распространённый стиль:
```kotlin
sealed interface NetworkResult {
data class Ok(val body: String) : NetworkResult
data class HttpError(val code: Int) : NetworkResult
data object Offline : NetworkResult
}
fun handle(r: NetworkResult) = when (r) {
is NetworkResult.Ok -> r.body
is NetworkResult.HttpError -> "HTTP ${r.code}"
NetworkResult.Offline -> "нет сети"
}
fun main() {
println(handle(NetworkResult.HttpError(404))) // HTTP 404
}
```
Плюс: имена не загрязняют пакет и читаются как `NetworkResult.Offline`.
## Исчерпывающий when
Проверка работает и для `when`-инструкции (без результата) - с Kotlin 2.0:
```kotlin
sealed interface Event
data object Start : Event
data object Stop : Event
fun log(e: Event) {
when (e) { // инструкция, не выражение
Start -> println("старт")
Stop -> println("стоп")
} // else не нужен, полнота проверяется
}
fun main() { log(Start) } // старт
```
Для nullable-типа нужна ветка `null`:
```kotlin
sealed interface Event
data object Start : Event
fun log(e: Event?) = when (e) {
Start -> "старт"
null -> "ничего"
}
fun main() { println(log(null)) } // ничего
```
## Иерархия глубже одного уровня
Промежуточный узел тоже может быть `sealed`:
```kotlin
sealed interface Expr
data class Num(val value: Int) : Expr
sealed interface BinOp : Expr {
val left: Expr
val right: Expr
}
data class Add(override val left: Expr, override val right: Expr) : BinOp
data class Mul(override val left: Expr, override val right: Expr) : BinOp
fun eval(e: Expr): Int = when (e) {
is Num -> e.value
is Add -> eval(e.left) + eval(e.right)
is Mul -> eval(e.left) * eval(e.right)
}
fun main() {
println(eval(Add(Num(2), Mul(Num(3), Num(4))))) // 14
}
```
Можно сопоставлять и на промежуточном уровне:
```kotlin
sealed interface Expr
data class Num(val value: Int) : Expr
sealed interface BinOp : Expr
data class Add(val l: Expr, val r: Expr) : BinOp
data class Mul(val l: Expr, val r: Expr) : BinOp
fun kind(e: Expr) = when (e) {
is Num -> "литерал"
is BinOp -> "операция" // покрывает Add и Mul
}
fun main() { println(kind(Add(Num(1), Num(2)))) } // операция
```
## Перебор наследников
```kotlin
sealed interface Status {
data object Active : Status
data object Blocked : Status
}
fun main() {
println(Status::class.sealedSubclasses.map { it.simpleName })
// [Active, Blocked]
}
```
Работает через рефлексию и требует `kotlin-reflect`. Для фиксированных объектов проще держать список руками. Подробнее: [Рефлексия](../../Рефлексия/Рефлексия.md)
## Типичные применения
**Результат операции** вместо исключений:
```kotlin
sealed interface Parsed<out T> {
data class Ok<T>(val value: T) : Parsed<T>
data class Err(val message: String) : Parsed<Nothing>
}
fun parse(s: String): Parsed<Int> =
s.toIntOrNull()?.let { Parsed.Ok(it) } ?: Parsed.Err("не число: $s")
fun main() {
println(when (val r = parse("abc")) {
is Parsed.Ok -> "значение ${r.value}"
is Parsed.Err -> r.message
}) // не число: abc
}
```
Обратите внимание на `Parsed<Nothing>` для варианта без данных - это работает благодаря `out T` и тому, что `Nothing` подтип всего. Подробнее: [Any, Unit, Nothing](Any,%20Unit,%20Nothing.md), [Вариантность](../../Дженерики/Вариантность.md)
**Состояние UI**, **события**, **команды**, **узлы AST** - везде, где набор вариантов фиксирован и у каждого свои данные.
> [!info] Отличия от Java
> В Java аналог появился в 17 - `sealed` с явным `permits`:
> ```java
> sealed interface Result permits Success, Failure {}
> record Success(String data) implements Result {}
> record Failure(Throwable error) implements Result {}
> ```
>
> | Java 17+ | Kotlin |
> |---|---|
> | `sealed interface X permits A, B` | `sealed interface X` (список выводится) |
> | наследники в том же пакете/модуле | то же |
> | `switch` с pattern matching (Java 21) | `when` с `is` |
> | проверка полноты в `switch` (Java 21) | есть всегда |
> | `non-sealed` для открытия ветки | нет аналога |
>
> Kotlin не требует перечислять наследников: компилятор находит их сам в пределах модуля. Это удобнее при добавлении варианта - править объявление не нужно.
>
> До Java 17 то же выражали абстрактным классом с приватным конструктором или паттерном Visitor. Visitor даёт проверку полноты (интерфейс с методом на каждый тип), но требует много кода и меняет все реализации при добавлении варианта.
>
> Сравнить: [Абстрактные классы в Java](../../../Программирование%20на%20Java/ООП/Абстрактные%20классы.md), [Полиморфизм подтипов](../../../Программирование%20на%20Java/ООП/Полиморфизм/Полиморфизм%20подтипов.md)
> [!warning] Подводные камни
> **`else` в `when` убивает проверку полноты.** Главная ошибка:
> ```kotlin
> sealed interface Event
> data object Start : Event
> data object Stop : Event
>
> fun log(e: Event) = when (e) {
> Start -> "старт"
> else -> "прочее" // добавление варианта пройдёт незамеченным
> }
>
> fun main() { println(log(Stop)) } // прочее
> ```
> Если ветвей много и большинство обрабатывается одинаково, лучше перечислить их через запятую, чем писать `else`.
>
> **Проверка полноты не работает из Java.** Java-код, читающий sealed-иерархию Kotlin, обязан иметь `default` в `switch`.
>
> **`when` по `is` без исчерпывающего контекста не проверяется.** Полнота требуется только там, где `when` - выражение или где компилятор знает, что иерархия закрыта. Для не-sealed типа проверки нет вообще.
>
> **`sealed class` требует вызова конструктора в наследнике:**
> ```kotlin
> sealed class Base(val id: Int)
> class Child(id: Int) : Base(id) // скобки обязательны
>
> fun main() { println(Child(1).id) } // 1
> ```
> У `sealed interface` конструктора нет - на одну причину для ошибки меньше.
>
> **Наследник в другом модуле не скомпилируется**, даже если пакет совпадает. Это ограничивает расширяемость библиотечного API: если вариантов должно быть больше, `sealed` не подходит.
>
> **`sealed` класс сам абстрактен** - создать его нельзя:
> ```kotlin
> sealed class Shape
> // val s = Shape() // ошибка: sealed-класс абстрактный
> class Circle : Shape()
>
> fun main() { println(Circle()::class.simpleName) } // Circle
> ```
>
> **Для варианта без данных нужен `data object`, а не `class`:**
> ```kotlin
> sealed interface State
> class Loading1 : State // каждый экземпляр разный
> data object Loading2 : State // единственный экземпляр
>
> fun main() {
> println(Loading1() == Loading1()) // false — ловушка
> println(Loading2 == Loading2) // true
> }
> ```
>
> **Дженерик-наследник sealed-типа требует внимания к вариантности.** Без `out` вариант без данных нельзя объявить как `Err : Parsed<Nothing>`.
Дополнительно: [Enum class](Enum%20class.md)
Далее: [Value class](Value%20class.md)
@@ -0,0 +1,228 @@
---
title: Typealias
description: Псевдоним типа для сокращения записи, его отличие от value class и границы применения
tags:
- kotlin
- types
---
`typealias` даёт типу другое имя. **Нового типа не создаётся** - это сокращение для чтения.
```kotlin
typealias Name = String
fun greet(name: Name) = "Привет, $name"
fun main() {
println(greet("Иван")) // Привет, Иван — обычная строка подходит
val n: Name = "Пётр"
val s: String = n // взаимозаменяемы
println(s)
}
```
## Где полезно
**Длинные дженерик-типы:**
```kotlin
typealias UsersByCity = Map<String, List<Pair<Int, String>>>
fun process(data: UsersByCity) = data.keys.sorted()
fun main() {
val data: UsersByCity = mapOf("Москва" to listOf(1 to "Иван"))
println(process(data)) // [Москва]
}
```
**Функциональные типы:**
```kotlin
typealias ClickHandler = (x: Int, y: Int) -> Unit
typealias Validator<T> = (T) -> String?
fun onClick(handler: ClickHandler) = handler(1, 2)
fun main() {
onClick { x, y -> println("клик $x,$y") } // клик 1,2
val notEmpty: Validator<String> =
{ if (it.isEmpty()) "пусто" else null }
println(notEmpty("")) // пусто
println(notEmpty("ok")) // null
}
```
Имена параметров в псевдониме функционального типа - документация: `(x: Int, y: Int) -> Unit` понятнее, чем `(Int, Int) -> Unit`.
**Разрешение конфликта имён при импорте:**
```kotlin
import java.util.Date
typealias SqlDate = java.sql.Date
fun main() {
val a: Date = Date()
val b: SqlDate = SqlDate(0)
println("${a::class.simpleName} ${b::class.simpleName}") // Date Date
}
```
Два класса с одинаковым простым именем нельзя импортировать оба - псевдоним решает это без полных имён в коде.
**Сокращение вложенных имён:**
```kotlin
class Outer {
class Inner {
class Deep { fun hello() = "привет" }
}
}
typealias Deep = Outer.Inner.Deep
fun main() {
println(Deep().hello()) // привет
}
```
## Дженерики
Псевдоним может иметь свои параметры типа:
```kotlin
typealias Handler<T> = (T) -> Unit
typealias Dict<V> = Map<String, V>
typealias IntDict = Dict<Int> // частичная подстановка
fun main() {
val h: Handler<String> = { println(it.uppercase()) }
h("привет") // ПРИВЕТ
val d: IntDict = mapOf("a" to 1)
println(d["a"]) // 1
}
```
## Только верхний уровень
```kotlin
typealias Ok = String // так
class Wrong {
// typealias Bad = String // ошибка: псевдоним только на верхнем уровне
}
fun main() {
val x: Ok = "ok"
println(x)
}
```
Внутри класса, функции или `object` объявить псевдоним нельзя.
## typealias и value class
Это часто путают, а задачи у них противоположные:
```kotlin
typealias UserId = Int
@JvmInline value class AccountId(val value: Int)
fun loadUser(id: UserId) = "user $id"
fun loadAccount(id: AccountId) = "account ${id.value}"
fun main() {
println(loadUser(42)) // 42 — любое Int подойдёт
println(loadAccount(AccountId(42))) // нужен именно AccountId
// println(loadAccount(42)) // не скомпилируется
}
```
| | `typealias` | `value class` |
|---|---|---|
| новый тип | нет | да |
| ошибку в аргументах ловит | нет | да |
| накладные расходы | нет | обычно нет |
| нужна распаковка (`.value`) | нет | да |
| можно добавить методы | нет | да |
| видно в байт-коде | нет | mangling имён |
Правило: если нужна **читаемость** - `typealias`. Если нужна **защита от перепутанных аргументов** - `value class`. Подробнее: [Value class](Value%20class.md)
## Псевдоним не скрывает реализацию
```kotlin
typealias Cache = HashMap<String, Int>
fun main() {
val c: Cache = HashMap()
println(c is HashMap<*, *>) // true — это и есть HashMap
println(c::class.simpleName) // HashMap
}
```
`typealias` нельзя использовать как абстракцию: клиент видит настоящий тип, полагается на его методы, и заменить реализацию не получится. Для этого нужен интерфейс.
## Когда не стоит
```kotlin
typealias S = String // экономия букв без смысла
typealias MyList = List<Int> // List<Int> и так понятен
```
Псевдоним добавляет уровень косвенности: читатель должен найти объявление, чтобы понять настоящий тип. Он оправдан, когда сокращает реально длинную запись или даёт имя смысловой роли (`Validator`, `ClickHandler`), а не когда просто короче.
> [!info] Отличия от Java
> В Java псевдонимов типов нет вообще. Длинные дженерик-типы приходится либо выписывать целиком, либо прятать за интерфейсом или наследованием:
> ```java
> // единственный способ "сократить" — создать настоящий тип
> class UsersByCity extends HashMap<String, List<Pair<Integer, String>>> {}
> ```
> Наследование от коллекции - плохая практика: тип получает весь API родителя и наружу просачивается реализация. Частичное облегчение даёт `var` (Java 10+), но только для локальных переменных:
> ```java
> var data = new HashMap<String, List<String>>(); // тип не пишем дважды
> ```
>
> | Java | Kotlin |
> |---|---|
> | нет аналога | `typealias X = Y` |
> | `extends HashMap<...>` как сокращение | `typealias` без наследования |
> | `var` для локальных переменных | вывод типов + `typealias` |
> | полное имя при конфликте импортов | `typealias` для одного из классов |
>
> Сравнить: [Параметрический полиморфизм в Java](../../../Программирование%20на%20Java/Дженерики/Параметрический%20полиморфизм.md)
> [!warning] Подводные камни
> **Типобезопасности нет.** Это главное заблуждение:
> ```kotlin
> typealias Meters = Int
> typealias Seconds = Int
>
> fun speed(distance: Meters, time: Seconds) = distance / time
>
> fun main() {
> println(speed(100, 10)) // 10
> val t: Seconds = 10
> val d: Meters = 100
> println(speed(t, d)) // 0 — аргументы перепутаны, компилятор молчит
> }
> ```
> Для настоящей защиты нужен `value class`.
>
> **Псевдоним не наследуется и не расширяется.** Добавить метод «к `Meters`» нельзя - расширение попадёт на `Int`:
> ```kotlin
> typealias Meters = Int
>
> fun Meters.describe() = "$this м" // это расширение Int!
>
> fun main() {
> println(42.describe()) // 42 м — работает для любого Int
> }
> ```
>
> **В сообщениях компилятора и в стектрейсах видно настоящий тип**, а не псевдоним. При разборе ошибки `expected Map<String, List<Pair<Int, String>>>` придётся сопоставлять его с `UsersByCity` вручную.
>
> **Из Java псевдоним не виден.** В сигнатурах, доступных Java-коду, стоит раскрытый тип.
>
> **Псевдоним на nullable-тип легко спутать:**
> ```kotlin
> typealias MaybeName = String?
>
> fun greet(n: MaybeName) = "Привет, ${n ?: "гость"}"
>
> fun main() {
> println(greet(null)) // Привет, гость
> }
> ```
> По имени `MaybeName` не очевидно, что тип nullable. Лучше, когда `?` виден в сигнатуре.
>
> **Нельзя объявить внутри класса или функции** - только на верхнем уровне файла.
Дополнительно: [Value class](Value%20class.md)
Далее: [equals, hashCode и toString](equals,%20hashCode%20и%20toString.md)
@@ -0,0 +1,265 @@
---
title: Value class
description: Обёртка без аллокации, типобезопасность примитивов, mangling имён и границы инлайнинга
tags:
- kotlin
- oop
- performance
---
`value class` - обёртка над одним значением, которая в байт-коде обычно **исчезает**: вместо объекта остаётся само значение.
```kotlin
@JvmInline
value class UserId(val value: Int)
fun load(id: UserId) = "пользователь ${id.value}"
fun main() {
val id = UserId(42)
println(load(id)) // пользователь 42
println(id.value) // 42
}
```
В скомпилированном коде `load` принимает `int`, а не объект. Аллокации нет.
Аннотация `@JvmInline` обязательна на JVM - без неё компилятор выдаст ошибку.
## Зачем
Основная причина - **типобезопасность без накладных расходов**. Классическая проблема:
```kotlin
fun transfer(from: Int, to: Int, amount: Int) = "$from -> $to: $amount"
fun main() {
println(transfer(100, 1, 2)) // перепутали порядок — компилятор молчит
}
```
Все параметры `Int`, поэтому их легко перепутать. С `value class` такая ошибка становится ошибкой компиляции:
```kotlin
@JvmInline value class AccountId(val value: Int)
@JvmInline value class Amount(val cents: Int)
fun transfer(from: AccountId, to: AccountId, amount: Amount) =
"${from.value} -> ${to.value}: ${amount.cents}"
fun main() {
println(transfer(AccountId(1), AccountId(2), Amount(100)))
// println(transfer(Amount(100), AccountId(1), AccountId(2))) // не скомпилируется
}
```
Цена - ноль: в байт-коде всё те же три `int`.
## Что можно внутри
```kotlin
@JvmInline
value class Email(val value: String) {
init {
require(value.contains("@")) { "неверный email: $value" }
}
val domain: String get() = value.substringAfter("@")
fun masked() = value.replaceRange(0, value.indexOf("@"), "***")
companion object {
fun parse(s: String): Email? = if (s.contains("@")) Email(s) else null
}
}
fun main() {
val e = Email("ivan@example.com")
println(e.domain) // example.com
println(e.masked()) // ***@example.com
println(Email.parse("нет")) // null
}
```
Разрешены: `init`, свойства с геттером, функции, `companion object`, реализация интерфейсов.
Запрещены: второе свойство с полем, `var`-свойства с полем, наследование, блоки инициализации полей, `lateinit`.
```kotlin
// @JvmInline value class Bad(val a: Int, val b: Int) // ошибка: только одно поле
// @JvmInline value class Bad2(var a: Int) // ошибка: нужен val
```
## Реализация интерфейса
```kotlin
interface Printable { fun print2(): String }
@JvmInline
value class Code(val value: Int) : Printable {
override fun print2() = "код $value"
}
fun main() {
println(Code(7).print2()) // код 7
val p: Printable = Code(7) // упаковка! см. ниже
println(p.print2()) // код 7
}
```
## Когда обёртка всё-таки создаётся
Инлайнинг не бесплатен в смысле предсказуемости: в некоторых позициях объект приходится создать.
| Позиция | Инлайнится |
|---|---|
| параметр и результат функции | да |
| локальная переменная | да |
| nullable-тип (`UserId?`) | **нет** |
| приведение к супертипу/интерфейсу | **нет** |
| элемент коллекции (`List<UserId>`) | **нет** - дженерики стираются |
| поле дженерик-класса | **нет** |
```kotlin
@JvmInline value class UserId(val value: Int)
fun main() {
val a: UserId = UserId(1) // int, без объекта
val b: UserId? = UserId(1) // объект UserId
val c: List<UserId> = listOf(UserId(1)) // объекты внутри списка
println("${a.value} ${b?.value} ${c.size}")
}
```
То есть `List<UserId>` по памяти не лучше `List<Integer>`. Выгода - в типобезопасности, а не в аллокациях.
## Mangling имён
Чтобы разные `value class` над одним типом не давали одинаковые JVM-сигнатуры, компилятор добавляет суффикс к имени метода:
```kotlin
@JvmInline value class AccountId(val value: Int)
fun process(id: AccountId) = id.value // в байт-коде: process-<хеш>(I)I
fun main() { println(process(AccountId(1))) } // 1
```
Из Java такую функцию вызвать практически нельзя - имя непредсказуемо. Решение - `@JvmName`:
```kotlin
@JvmInline value class AccountId(val value: Int)
@JvmName("processAccount")
fun process(id: AccountId) = id.value
fun main() { println(process(AccountId(1))) } // 1
```
Подробнее: [Kotlin из Java](../../Взаимодействие%20с%20Java/Kotlin%20из%20Java.md)
## Равенство
Сравнение идёт по содержимому, причём без упаковки:
```kotlin
@JvmInline value class UserId(val value: Int)
fun main() {
println(UserId(1) == UserId(1)) // true
println(UserId(1) == UserId(2)) // false
println(setOf(UserId(1), UserId(1)).size) // 1
}
```
`equals`/`hashCode` генерируются автоматически и переопределять их нельзя.
## value class и data class
```kotlin
@JvmInline value class Meters(val value: Double)
data class Point(val x: Int, val y: Int)
```
`value class` - одно поле, без аллокации, нет `copy`. `data class` - любое число полей, всегда объект, есть `copy` и `componentN`.
Для одного поля `data class` тоже работает, но создаёт объект:
```kotlin
data class SlowId(val value: Int) // объект на каждый вызов
@JvmInline value class FastId(val value: Int) // int
fun main() {
println(SlowId(1) == SlowId(1)) // true, но два объекта были созданы
println(FastId(1) == FastId(1)) // true, объектов не было
}
```
## typealias - не замена
```kotlin
typealias UserId2 = Int
fun load(id: UserId2) = id
fun main() {
println(load(42)) // 42 — любое Int подходит, защиты нет
}
```
`typealias` - только псевдоним, он не создаёт новый тип и не защищает от путаницы. Подробнее: [Typealias](Typealias.md)
> [!info] Отличия от Java
> Прямого аналога нет. Java-разработчик выбирает между:
> - **обычным классом-обёрткой** - типобезопасно, но аллокация на каждый объект
> - **примитивом с комментарием** - быстро, но ошибки порядка аргументов не ловятся
>
> ```java
> // обёртка: безопасно, но объект
> final class UserId {
> private final int value;
> UserId(int value) { this.value = value; }
> // equals, hashCode, геттер...
> }
> ```
>
> В Java это закрывает Project Valhalla (`value class` в JEP 401), но он ещё не вышел в релиз. Пока единственный способ получить безопасность без аллокации - `value class` Kotlin.
>
> | Java | Kotlin |
> |---|---|
> | класс-обёртка + аллокация | `@JvmInline value class` |
> | `int` напрямую | `value class` с тем же байт-кодом |
> | Valhalla (в разработке) | работает с Kotlin 1.5 |
>
> Сравнить: [Классы-обертки в Java](../../../Программирование%20на%20Java/Синтаксис%20и%20типы/Базовые%20типы%20ООП/Классы-обертки.md)
> [!warning] Подводные камни
> **`@JvmInline` обязательна.** Без неё на JVM компилятор выдаст ошибку, а не предупреждение:
> ```kotlin
> // value class Bad(val x: Int) // error: value class without @JvmInline
> @JvmInline value class Good(val x: Int)
>
> fun main() { println(Good(1).x) }
> ```
>
> **Nullable-тип отменяет инлайнинг.** `UserId?` - настоящий объект:
> ```kotlin
> @JvmInline value class UserId(val value: Int)
>
> fun find(id: UserId?): String = id?.value?.toString() ?: "нет"
>
> fun main() { println(find(UserId(1))) } // 1 — но обёртка была создана
> ```
> В горячем коде вместо `UserId?` лучше использовать значение-маркер.
>
> **В коллекциях выгоды нет.** `List<UserId>` хранит объекты из-за стирания типов.
>
> **`init` вызывается, но не защищает от Java.** Java-код может создать экземпляр в обход проверки, потому что на уровне байт-кода это просто `int`:
> ```kotlin
> @JvmInline
> value class Age(val value: Int) {
> init { require(value >= 0) { "возраст отрицательный" } }
> }
>
> fun main() {
> println(Age(5).value) // 5
> // Age(-1) // IllegalArgumentException из Kotlin
> }
> ```
>
> **Mangling ломает вызов из Java и из фреймворков, работающих через рефлексию.** Jackson, Spring, JPA требуют отдельной настройки для `value class`; иногда он просто не поддерживается.
>
> **Переопределить `equals`, `hashCode` нельзя** - они генерируются. `toString` переопределить можно.
>
> **Два `value class` над одним типом взаимозаменяемы в байт-коде.** Приведение через `Any` обойдёт проверку типов:
> ```kotlin
> @JvmInline value class Meters(val v: Int)
> @JvmInline value class Feet(val v: Int)
>
> fun main() {
> val m = Meters(10)
> val any: Any = m
> println((any as Meters).v) // 10
> // println((any as Feet).v) // ClassCastException — на уровне Kotlin проверка есть
> }
> ```
>
> **Нельзя наследоваться и нельзя быть `open`** - иерархии из `value class` не построить.
Дополнительно: [Typealias](Typealias.md)
Далее: [Object и companion object](Object%20и%20companion%20object.md)
@@ -0,0 +1,320 @@
---
title: equals, hashCode и toString
description: Контракт равенства, == против ===, правила hashCode и как не сломать HashMap
tags:
- kotlin
- oop
---
Три метода объявлены в [Any](Any,%20Unit,%20Nothing.md) и есть у каждого объекта. Их реализация по умолчанию - по ссылке и по адресу в памяти.
```kotlin
class Point(val x: Int, val y: Int)
fun main() {
val a = Point(1, 2)
val b = Point(1, 2)
println(a == b) // false — equals по ссылке
println(a) // Point@1b6d3586 — toString по хешу
}
```
## == и ===
```kotlin
fun main() {
val a = "Kotlin"
val b = StringBuilder("Kotlin").toString()
println(a == b) // true — структурное равенство, вызывает equals
println(a === b) // false — ссылочное равенство, тот же объект?
println(a !== b) // true
}
```
| Оператор | Что делает |
|---|---|
| `==` | `a?.equals(b) ?: (b === null)` |
| `===` | сравнение ссылок (для примитивов - значений) |
`==` безопасен для `null`: левый операнд может быть `null`, NPE не будет.
```kotlin
fun main() {
val a: String? = null
println(a == "x") // false — без исключения
println(a == null) // true
println(null == null) // true
}
```
## Контракт equals
Реализация должна быть:
- **рефлексивной**: `a == a` всегда `true`
- **симметричной**: `a == b` тогда и только тогда, когда `b == a`
- **транзитивной**: `a == b` и `b == c` влечёт `a == c`
- **согласованной**: результат не меняется, пока объекты не изменились
- `a == null` всегда `false`
```kotlin
class Point(val x: Int, val y: Int) {
override fun equals(other: Any?): Boolean {
if (this === other) return true // быстрый путь
if (other !is Point) return false // проверка типа и null сразу
return x == other.x && y == other.y
}
override fun hashCode(): Int = 31 * x + y
override fun toString() = "Point($x, $y)"
}
fun main() {
val a = Point(1, 2)
println(a == Point(1, 2)) // true
println(a == null) // false
println(a) // Point(1, 2)
}
```
`other !is Point` закрывает и `null`, и неверный тип: `is` для `null` даёт `false`.
Параметр обязан иметь тип `Any?` - иначе это не переопределение, а перегрузка:
```kotlin
class Bad(val x: Int) {
fun equals(other: Bad) = x == other.x // перегрузка! override не стоит
}
fun main() {
val a: Any = Bad(1)
println(a == Bad(1)) // false — вызвался equals из Any
}
```
Компилятор не позволит поставить `override` на такую функцию - это и есть подсказка.
## Контракт hashCode
Единственное жёсткое правило: **равные объекты обязаны иметь равный хеш**.
```kotlin
class Broken(val x: Int) {
override fun equals(other: Any?) = other is Broken && other.x == x
// hashCode не переопределён — контракт нарушен
}
fun main() {
val a = Broken(1)
val set = hashSetOf(a)
println(set.contains(Broken(1))) // false — объект "потерян"
println(a == Broken(1)) // true — хотя equals говорит "равны"
}
```
`HashSet` сначала ищет бакет по хешу. Хеши разные - поиск даже не доходит до `equals`.
Обратное неверно: разные объекты **могут** иметь одинаковый хеш (коллизия) - это нормально и лишь замедляет поиск.
```kotlin
class Pair2(val a: Int, val b: Int) {
override fun equals(other: Any?) = other is Pair2 && other.a == a && other.b == b
override fun hashCode() = 31 * a + b
}
fun main() {
println(Pair2(1, 32).hashCode()) // 63
println(Pair2(2, 1).hashCode()) // 63 — коллизия, но всё корректно
println(Pair2(1, 32) == Pair2(2, 1)) // false
}
```
Стандартный способ считать хеш по нескольким полям:
```kotlin
class User(val id: Int, val name: String, val tags: List<String>) {
override fun equals(other: Any?) =
other is User && other.id == id && other.name == name && other.tags == tags
override fun hashCode(): Int {
var result = id
result = 31 * result + name.hashCode()
result = 31 * result + tags.hashCode()
return result
}
}
fun main() {
val a = User(1, "Иван", listOf("admin"))
println(a == User(1, "Иван", listOf("admin"))) // true
println(a.hashCode() == User(1, "Иван", listOf("admin")).hashCode()) // true
}
```
Множитель 31 - соглашение из JDK: простое число, а умножение на него оптимизируется в сдвиг и вычитание.
Короче - через `Objects.hash`, но он создаёт массив:
```kotlin
import java.util.Objects
class User(val id: Int, val name: String) {
override fun equals(other: Any?) = other is User && other.id == id && other.name == name
override fun hashCode() = Objects.hash(id, name)
}
fun main() { println(User(1, "a") == User(1, "a")) } // true
```
## data class делает это за вас
```kotlin
data class Point(val x: Int, val y: Int)
fun main() {
val a = Point(1, 2)
println(a == Point(1, 2)) // true
println(a) // Point(x=1, y=2)
println(hashSetOf(a).contains(Point(1, 2))) // true
}
```
В большинстве случаев ручная реализация не нужна - достаточно `data class`. Писать руками приходится, когда: равенство только по части полей, в классе есть массив, или класс - сущность с идентификатором. Подробнее: [Data class](Data%20class.md)
## toString
Единственный из трёх, у которого нет контракта - но есть ожидания. Он попадает в логи и в сообщения об ошибках:
```kotlin
class Order(val id: Int, val items: List<String>) {
override fun toString() = "Order(id=$id, items=${items.size})"
}
fun main() {
println(Order(1, listOf("a", "b"))) // Order(id=1, items=2)
println("заказ: ${Order(1, emptyList())}") // заказ: Order(id=1, items=0)
println(listOf(Order(1, emptyList()))) // [Order(id=1, items=0)]
}
```
Строковый шаблон, конкатенация и печать коллекции - все вызывают `toString`. Поэтому класс без нормального `toString` превращает лог в набор адресов.
## Равенство и наследование
Строгая симметричность при наследовании достигается проверкой точного класса:
```kotlin
open class Base(val x: Int) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || this::class != other::class) return false
return x == (other as Base).x
}
override fun hashCode() = x
}
class Derived(x: Int, val y: Int) : Base(x) {
override fun equals(other: Any?) =
super.equals(other) && y == (other as Derived).y
override fun hashCode() = 31 * super.hashCode() + y
}
fun main() {
println(Base(1) == Derived(1, 2)) // false — разные классы
println(Derived(1, 2) == Derived(1, 2)) // true
}
```
С `other is Base` вместо сравнения классов получилась бы асимметрия: `base == derived` истина, `derived == base` ложь. Это нарушение контракта, которое ломает коллекции.
Проще всего не наследоваться там, где важно равенство - именно поэтому `data class` нельзя сделать `open`.
## Равенство примитивов и упаковка
```kotlin
fun main() {
val a: Int = 1000
val b: Int = 1000
println(a == b) // true
println(a === b) // true — примитивы, сравнение значений
val c: Int? = 1000
val d: Int? = 1000
println(c == d) // true
println(c === d) // false! разные объекты Integer
}
```
Nullable-число упаковывается в `Integer`, и `===` сравнивает ссылки. Для значений от -128 до 127 работает кеш JVM, и `===` даёт `true` - то есть результат зависит от величины числа. Для чисел используйте только `==`.
> [!info] Отличия от Java
> | Java | Kotlin |
> |---|---|
> | `a.equals(b)` | `a == b` |
> | `a == b` (ссылки) | `a === b` |
> | `Objects.equals(a, b)` для null | `a == b` |
> | `@Override boolean equals(Object o)` | `override fun equals(other: Any?)` |
> | `o instanceof Point p` (Java 16+) | `other is Point` + умное приведение |
> | `Objects.hash(a, b)` | то же, или вручную |
> | генерация в IDE или Lombok | `data class` |
> | `String.valueOf(obj)` для null | `obj.toString()` работает на `null?` |
>
> **Главное: `==` в Kotlin вызывает `equals`.** Это убирает самый частый класс ошибок Java - сравнение строк через `==`, которое иногда «работает» из-за пула строк и ломается на строках, собранных в рантайме.
>
> **`==` не бросает NPE**, в отличие от `a.equals(b)` в Java. Обёртка `Objects.equals` больше не нужна.
>
> **`toString()` можно звать на `null`:**
> ```kotlin
> fun main() {
> val s: String? = null
> println(s.toString()) // "null" — расширение Any?.toString()
> }
> ```
> В Java `null.toString()` - это NPE, нужен `String.valueOf`.
>
> **`equals` в Kotlin не перегрузить случайно.** Параметр с типом `Any?` обязателен для `override`, поэтому ошибка «написал `equals(Point)` вместо `equals(Object)`» ловится компилятором.
>
> Сравнить: [Контракт между equals и hashCode](../../../Программирование%20на%20Java/Синтаксис%20и%20типы/Базовые%20типы%20ООП/Контракт%20между%20equals%20и%20hashCode.md), [HashMap в Java](../../../Программирование%20на%20Java/Коллекции/HashMap.md)
> [!warning] Подводные камни
> **Переопределить `equals` без `hashCode`** - значит получить объект, который теряется в `HashSet` и `HashMap`. IDE предупреждает, компилятор - нет.
>
> **Изменяемый объект как ключ `HashMap`:**
> ```kotlin
> data class Key(var id: Int)
>
> fun main() {
> val key = Key(1)
> val map = hashMapOf(key to "значение")
> key.id = 2
> println(map[key]) // null — ищем в новом бакете
> println(map.keys.first()) // Key(id=2) — запись на месте, но недоступна
> println(map.size) // 1
> }
> ```
> Запись становится невидимой и неудаляемой. Ключи обязаны быть неизменяемыми.
>
> **Массив внутри класса ломает `equals` по содержимому:**
> ```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
> }
> ```
> Без `contentEquals`/`contentHashCode` сравнение идёт по ссылке. Подробнее: [Массивы](../Работа%20с%20данными/Массивы.md)
>
> **`===` для чисел зависит от значения:**
> ```kotlin
> fun main() {
> val a: Int? = 127
> val b: Int? = 127
> println(a === b) // true — из кеша Integer
> val c: Int? = 128
> val d: Int? = 128
> println(c === d) // false — за пределами кеша
> }
> ```
> Это не особенность Kotlin, а кеш `Integer.valueOf` в JVM. Вывод: `===` на числах не использовать.
>
> **`equals` с `is` вместо проверки класса даёт асимметрию при наследовании** - и вместе с ней непредсказуемое поведение коллекций.
>
> **`hashCode`, зависящий от изменяемого поля, нарушает согласованность.** Объект должен давать один и тот же хеш всё время, пока лежит в хеш-коллекции.
>
> **`toString` печатает всё, включая секреты:**
> ```kotlin
> data class Token(val value: String) {
> override fun toString() = "Token(***)"
> }
>
> fun main() { println(Token("secret-abc")) } // Token(***)
> ```
> `data class` с паролем или токеном попадёт в лог целиком, если не переопределить `toString`.
>
> **`hashCode` не обязан быть стабильным между запусками JVM.** `Object.hashCode` зависит от адреса, `String.hashCode` стабилен по спецификации, но для своего класса гарантий нет - сохранять хеш в БД нельзя.
Дополнительно: [Data class](Data%20class.md)
Далее: [ООП](../../ООП/ООП.md)