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
@@ -155,6 +155,8 @@ println("abc".toIntOrNull()) // null — вместо исключения
> | три вложенных `if` для цепочки | `a?.b?.c` |
>
> Java-аналог `Optional` требует оборачивания и разворачивания, работает только как возвращаемое значение и создаёт объект. Операторы Kotlin компилируются в обычные проверки `ifnull` в байт-коде - накладных расходов нет.
>
> Сравнить: [Приведение ссылочных типов в Java](../../../Программирование%20на%20Java/Синтаксис%20и%20типы/Работа%20с%20данными%20и%20типами/Приведение%20ссылочных%20типов.md)
> [!warning] Подводные камни
> **`?.` возвращает nullable, цепочка «заражает» весь результат:**
@@ -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)
@@ -113,6 +113,8 @@ fun helper() { } // из Java: Utils.helper()
> | `package-private` по умолчанию | `public` по умолчанию |
>
> Последняя строка важна: в Java класс без модификатора виден только своему пакету, в Kotlin - всем. Для сокрытия внутренностей библиотеки используют `internal`.
>
> Сравнить: [Модификаторы в Java](../../../Программирование%20на%20Java/ООП/Инкапсуляция/Модификаторы.md)
> [!warning] Подводные камни
> **Нет аналога package-private.** `internal` ограничивает *модулем* (единицей компиляции Gradle), а не пакетом. Разделить код на пакеты и спрятать их друг от друга внутри одного модуля нельзя - только разносить по модулям.
@@ -0,0 +1,140 @@
---
title: Char
description: Char как отдельный тип, код символа, проверки категорий и работа с цифрами
tags:
- kotlin
- stdlib
---
`Char` - один символ в одинарных кавычках. Хранит кодовую единицу UTF-16 (16 бит).
```kotlin
val c: Char = 'A'
val tab = '\t'
val quote = '\''
val unicode = '\u00A9' // ©
```
Ключевое отличие от Java: **`Char` не является числом**. Арифметику с ним делать можно, но неявного превращения в `Int` нет.
```kotlin
val c = 'A'
// val n: Int = c // ошибка компиляции
val n: Int = c.code // 65 — явно
println(c + 1) // B — Char + Int = Char
println('z' - 'a') // 25 — Char - Char = Int
println(c.toString()) // A
```
| Выражение | Результат | Тип |
|---|---|---|
| `'A' + 1` | `'B'` | `Char` |
| `'z' - 'a'` | `25` | `Int` |
| `'A'.code` | `65` | `Int` |
| `65.toChar()` | `'A'` | `Char` |
## Проверки категорий
```kotlin
println('a'.isLetter()) // true
println('5'.isDigit()) // true
println('a'.isLetterOrDigit()) // true
println(' '.isWhitespace()) // true
println('a'.isLowerCase()) // true
println('A'.isUpperCase()) // true
```
Все эти функции учитывают Unicode: `'я'.isLetter()` тоже `true`.
## Регистр
```kotlin
println('a'.uppercaseChar()) // A — возвращает Char
println('A'.lowercaseChar()) // a
println('a'.uppercase()) // "A" — возвращает String
println('ß'.uppercase()) // "SS" — два символа, поэтому String
```
Есть две версии: `uppercaseChar()` даёт `Char`, `uppercase()` - `String`. Вторая нужна, потому что в Unicode регистр не всегда меняется один-к-одному.
## Цифры
```kotlin
println('7'.digitToInt()) // 7 — число, а не код символа
println('f'.digitToInt(16)) // 15
println('x'.digitToIntOrNull()) // null — безопасная версия
println(7.digitToChar()) // '7'
```
Это главный способ разобрать цифру. Старый трюк из Java (`c - '0'`) тоже работает, но `digitToInt()` понятнее и проверяет корректность:
```kotlin
println('7' - '0') // 7 — работает
// println('x'.digitToInt()) // IllegalArgumentException
```
## Диапазоны и сравнение
```kotlin
println('a' in 'a'..'z') // true
println('5' in '0'..'9') // true
for (c in 'a'..'e') print(c) // abcde
println('a' < 'b') // true — по коду
println(('a'..'e').toList()) // [a, b, c, d, e]
```
## Строки и Char
```kotlin
val s = "Kotlin"
println(s[0]) // K — элемент строки это Char
println(s.first()) // K
println(s.toCharArray().size) // 6
println(String(charArrayOf('a','b'))) // ab — собрать строку из массива
println(s.toList()) // [K, o, t, l, i, n]
println("abc".map { it + 1 }) // [b, c, d]
println(charArrayOf('a','b').concatToString()) // ab
```
Приписать `Char` к строке можно сложением - слева обязательно строка:
```kotlin
println("ab" + 'c') // abc
// println('c' + "ab") // ошибка: Char.plus ожидает Int
println("${'c'}ab") // cab — через шаблон
```
> [!info] Отличия от Java
> | Java | Kotlin |
> |---|---|
> | `char` - числовой тип | `Char` - отдельный тип |
> | `int n = c;` работает | нужен `c.code` |
> | `Character.isDigit(c)` | `c.isDigit()` |
> | `Character.toUpperCase(c)` | `c.uppercaseChar()` |
> | `Character.getNumericValue(c)` | `c.digitToInt()` |
> | `c - '0'` для цифры | `c.digitToInt()` |
> | `switch (c)` по числам | `when (c)` по символам |
>
> В Java `char` участвует в арифметике как число, из-за чего `System.out.println('a' + 1)` печатает `98`, а не `b`. В Kotlin тот же код даёт `b` - тип сохраняется.
>
> Все утилиты из `Character` стали методами `Char`, поэтому пишутся через точку и находятся автодополнением.
>
> Сравнить: [Character в Java](../../../Программирование%20на%20Java/Синтаксис%20и%20типы/Работа%20с%20данными%20и%20типами/Character.md)
> [!warning] Подводные камни
> **`Char` не вмещает символы за пределами BMP.** Эмодзи и редкие иероглифы кодируются двумя `Char` (surrogate pair):
> ```kotlin
> val emoji = "😀"
> println(emoji.length) // 2
> println(emoji[0].isLetter()) // false — половина пары бессмысленна
> println(emoji.codePointAt(0)) // 128512 — полный код символа
> ```
> Для посимвольной обработки текста с эмодзи нужны кодовые точки, а не `Char`.
>
> **`digitToInt()` бросает исключение, а не возвращает -1:**
> ```kotlin
> println('x'.digitToIntOrNull()) // null — безопасно
> // println('x'.digitToInt()) // IllegalArgumentException
> ```
>
> **`'1'.code` - это 49, а не 1.** Путать код символа с его числовым значением - классическая ошибка:
> ```kotlin
> println('1'.code) // 49
> println('1'.digitToInt()) // 1
> ```
>
> **`Char` в `when` требует `else`** - перечислить все 65 536 значений компилятор не может.
>
> **`uppercase()` возвращает `String`, и это не придирка.** `'ß'.uppercase()` даёт `"SS"` - два символа. Если нужен именно `Char`, используйте `uppercaseChar()`, но помните, что для `'ß'` он вернёт сам `'ß'`.
Далее: [Числа и переполнение](Числа%20и%20переполнение.md)
@@ -0,0 +1,218 @@
---
title: Деструктуризация
description: Разбор объекта на переменные, componentN, деструктуризация в лямбдах и циклах
tags:
- kotlin
- syntax
---
Деструктуризация - разбор объекта на отдельные переменные одной конструкцией.
```kotlin
data class Point(val x: Int, val y: Int)
fun main() {
val (x, y) = Point(1, 2)
println("$x $y") // 1 2
}
```
Компилятор превращает это в вызовы `component1()`, `component2()` и так далее:
```kotlin
data class Point(val x: Int, val y: Int)
fun main() {
val p = Point(1, 2)
val x = p.component1()
val y = p.component2()
println("$x $y") // 1 2
}
```
## Что можно деструктурировать
Любой тип, у которого есть функции `componentN()`. Автоматически они появляются у:
- [data class](../Базовые%20типы%20ООП/Data%20class.md) - по свойствам основного конструктора
- `Pair` и `Triple`
- `Map.Entry` - ключ и значение
- списков и массивов (первые 5 элементов)
```kotlin
fun main() {
val (a, b) = Pair(1, "x")
val (c, d, e) = Triple(1, 2, 3)
val (first, second) = listOf("a", "b", "c") // третий игнорируется
println("$a $b | $c $d $e | $first $second")
}
```
## Свой componentN
Для обычного класса операторы объявляются вручную:
```kotlin
class Range2(val from: Int, val to: Int) {
operator fun component1() = from
operator fun component2() = to
}
fun main() {
val (from, to) = Range2(1, 10)
println("$from..$to") // 1..10
}
```
Слово `operator` обязательно.
## Пропуск компонентов
```kotlin
data class User(val id: Int, val name: String, val email: String)
fun main() {
val (_, name, _) = User(1, "Иван", "i@x.ru")
println(name) // Иван
}
```
Подчёркивание означает «этот компонент не нужен» - `componentN()` для него даже не вызовется.
Если нужны только первые компоненты, лишние просто не перечисляют:
```kotlin
data class User(val id: Int, val name: String, val email: String)
fun main() {
val (id, name) = User(1, "Иван", "i@x.ru") // email не берём
println("$id $name") // 1 Иван
}
```
## В циклах
```kotlin
fun main() {
val map = mapOf("a" to 1, "b" to 2)
for ((key, value) in map) {
println("$key=$value") // a=1, затем b=2
}
val list = listOf("x", "y")
for ((index, item) in list.withIndex()) {
println("$index:$item") // 0:x, затем 1:y
}
}
```
Это самое частое применение деструктуризации.
## В лямбдах
```kotlin
fun main() {
val map = mapOf("a" to 1, "b" to 2)
map.forEach { (k, v) -> println("$k$v") } // a1 b2
println(map.map { (k, v) -> "$k=$v" }) // [a=1, b=2]
map.forEach { (_, v) -> print(v) } // 12
println()
val points = listOf(1 to 2, 3 to 4)
println(points.map { (x, y) -> x + y }) // [3, 7]
}
```
Параметр в скобках `{ (k, v) -> }` - это **один** параметр, который разбирается, а не два отдельных. Без скобок `{ k, v -> }` лямбда ожидала бы два аргумента, и код не скомпилировался бы.
## Возврат нескольких значений
Классическое применение - функция, которая должна вернуть пару значений:
```kotlin
data class Result2(val value: Int, val error: String?)
fun parse(s: String): Result2 =
s.toIntOrNull()?.let { Result2(it, null) } ?: Result2(0, "не число")
fun main() {
val (value, error) = parse("42")
println("$value $error") // 42 null
val (v2, e2) = parse("abc")
println("$v2 $e2") // 0 не число
}
```
`Pair` для этого тоже подходит, но свой `data class` лучше: имена компонентов осмысленны, а не `first`/`second`.
## Деструктуризация по именам (Kotlin 2.3+)
Классическая деструктуризация позиционная - порядок переменных должен совпадать с порядком свойств. Это хрупко: перестановка свойств в `data class` молча меняет смысл кода.
С Kotlin 2.3 разрабатывается деструктуризация по именам. Синтаксис меняется так:
- `val [a, b] = obj` - **квадратные** скобки, позиционно (как было)
- `val (name, email) = obj` - **круглые** скобки, теперь **по именам** свойств
```kotlin
data class User(val name: String, val email: String)
fun main() {
val user = User("ИМЯ", "ПОЧТА")
val (email, name) = user // по именам — порядок не важен
println("name=$name email=$email") // name=ИМЯ email=ПОЧТА
val [first, second] = user // позиционно
println("first=$first") // first=ИМЯ
}
```
Фича пока за флагом компилятора:
```bash
kotlinc -Xname-based-destructuring=complete App.kt
```
Значения флага: `only-syntax` (включить синтаксис `[]` и полную форму), `name-mismatch` (предупреждать, когда имена в позиционной деструктуризации не совпадают со свойствами), `complete` (включить короткую форму по именам).
> [!warning] Это ломающее изменение
> Круглые скобки меняют смысл: был порядок, станут имена. Код `val (email, name) = user` сегодня присвоит `email` первое свойство, а после включения фичи - свойство с именем `email`. Пока флаг не включён, вся деструктуризация в вашем коде позиционная.
> [!info] Отличия от Java
> В Java деструктуризации объектов нет. Ближайший аналог - record patterns из Java 21:
> ```java
> record Point(int x, int y) {}
> if (obj instanceof Point(int x, int y)) { ... }
> ```
> Но это работает только в `instanceof` и `switch`, а не при обычном объявлении переменных. Разбор `Map.Entry` в цикле в Java невозможен - нужно писать `e.getKey()` и `e.getValue()`.
>
> | Java | Kotlin |
> |---|---|
> | `var k = e.getKey(); var v = e.getValue();` | `val (k, v) = e` |
> | `for (var e : map.entrySet())` + геттеры | `for ((k, v) in map)` |
> | `p.x(); p.y();` | `val (x, y) = p` |
> | нет возврата нескольких значений | `Pair`, `Triple`, `data class` |
>
> Сравнить: [Record в Java](../../../Программирование%20на%20Java/Синтаксис%20и%20типы/Базовые%20типы%20ООП/Record.md)
> [!warning] Подводные камни
> **Деструктуризация позиционная, а не по именам.** Это главная опасность:
> ```kotlin
> data class User(val name: String, val email: String)
>
> fun main() {
> val (name, email) = User("Иван", "i@x.ru")
> println("$name / $email") // Иван / i@x.ru
> }
> ```
> Если позже в `data class` поменять порядок свойств на `(email, name)`, этот код **продолжит компилироваться**, но переменные получат чужие значения. Компилятор не поможет, потому что типы совпадают. Поэтому порядок свойств в `data class` - часть публичного API.
>
> **У обычного класса деструктуризации нет** - только у `data class` или при ручном объявлении `componentN()`.
>
> **В лямбде нужны скобки:**
> ```kotlin
> fun main() {
> val map = mapOf("a" to 1)
> map.forEach { (k, v) -> println("$k$v") } // верно: один параметр-пара
> map.forEach { e -> println("${e.key}${e.value}") } // тоже верно
> }
> ```
> Запись `{ k, v -> }` для `forEach` на `Map` не скомпилируется.
>
> **Список короче числа переменных - исключение в рантайме:**
> ```kotlin
> fun main() {
> val list = listOf(1)
> val (a, b) = list // компилируется!
> println("$a $b") // IndexOutOfBoundsException
> }
> ```
> У `List` есть `component1()`..`component5()`, и компилятор не знает фактическую длину. С `data class` такой проблемы нет.
>
> **`component1()` вызывается даже для неиспользуемой переменной** - если это не `_`. Для свойства с тяжёлым геттером лишняя работа.
>
> **Деструктуризация `Pair` теряет смысл имён.** `val (a, b) = getCoordinates()` не говорит, что где. Лучше `data class Coordinates(val lat: Double, val lon: Double)`.
Далее: [Приведение типов](Приведение%20типов.md)
@@ -0,0 +1,273 @@
---
title: Лямбды и функциональные типы
description: Синтаксис лямбд, it, функциональные типы, ссылки на функции и анонимные функции
tags:
- kotlin
- functional
---
Функциональный тип описывает функцию как значение: `(параметры) -> результат`.
```kotlin
val square: (Int) -> Int = { x -> x * x }
val greet: (String) -> String = { "Привет, $it" }
val action: () -> Unit = { println("сделано") }
val sum: (Int, Int) -> Int = { a, b -> a + b }
fun main() {
println(square(4)) // 16
println(greet("мир")) // Привет, мир
action() // сделано
println(sum(2, 3)) // 5
}
```
## Синтаксис лямбды
Лямбда всегда в фигурных скобках:
```kotlin
{ параметры -> тело }
```
```kotlin
val f = { x: Int, y: Int -> x + y } // типы указаны в лямбде
val g: (Int) -> Int = { it * 2 } // тип указан у переменной
```
Тип нужно указать либо у переменной, либо у параметров лямбды - где-то компилятор должен его увидеть.
Результат - **последнее выражение** тела, `return` не нужен и в обычной лямбде запрещён:
```kotlin
val describe: (Int) -> String = { n ->
val prefix = if (n > 0) "плюс" else "минус"
"$prefix $n" // это и есть результат
}
fun main() { println(describe(5)) } // плюс 5
```
## it - неявный параметр
Если параметр один, его можно не объявлять:
```kotlin
val double: (Int) -> Int = { it * 2 }
println(listOf(1, 2, 3).map { it * 10 }) // [10, 20, 30]
```
`it` удобен в короткой лямбде. Если тело длинное или лямбды вложены - давайте имя:
```kotlin
val users = listOf("Иван", "Пётр")
println(users.map { name -> name.uppercase() }) // [ИВАН, ПЁТР]
```
## Лямбда последним аргументом
Если последний параметр функции - функция, лямбду выносят за скобки:
```kotlin
fun measure(name: String, block: () -> Unit) {
val start = System.nanoTime()
block()
println("$name занял ${System.nanoTime() - start} нс")
}
fun main() {
measure("цикл") { // лямбда вне скобок
repeat(1000) { }
}
measure("пусто", { }) // так тоже можно, но не принято
}
```
Когда функция принимает **только** лямбду, круглые скобки опускаются совсем. Именно на этом правиле построены `forEach`, `let`, `apply` и все DSL:
```kotlin
listOf(1, 2).forEach { println(it) }
```
Подробнее: [DSL](../../Функциональный%20Kotlin/DSL.md)
## Неиспользуемые параметры
```kotlin
val map = mapOf("a" to 1)
map.forEach { (_, value) -> println(value) } // 1 — ключ не нужен
```
Подчёркивание говорит компилятору, что параметр намеренно игнорируется.
## Ссылки на функции - ::
Вместо лямбды, которая только вызывает существующую функцию, используют ссылку:
```kotlin
fun isEven(n: Int) = n % 2 == 0
fun main() {
val nums = listOf(1, 2, 3, 4)
println(nums.filter { isEven(it) }) // [2, 4] — лямбда-обёртка
println(nums.filter(::isEven)) // [2, 4] — ссылка, короче
nums.forEach(::println)
}
```
Виды ссылок:
```kotlin
class User(val name: String) {
fun greet() = "Привет, $name"
}
fun main() {
val byName: (User) -> String = User::name // ссылка на свойство
val greeter: (User) -> String = User::greet // на метод класса
val factory: (String) -> User = ::User // на конструктор
val u = User("Иван")
println(byName(u)) // Иван
println(greeter(u)) // Привет, Иван
println(factory("Пётр").name) // Пётр
val bound = u::greet // привязанная к объекту
println(bound()) // Привет, Иван
}
```
## Анонимные функции
Когда нужен явный `return` или явный тип результата:
```kotlin
val parse = fun(s: String): Int? {
if (s.isBlank()) return null // return работает
return s.toIntOrNull()
}
fun main() {
println(parse(" ")) // null
println(parse("42")) // 42
}
```
Отличие от лямбды: `return` выходит из самой анонимной функции, а в лямбде он запрещён (или выходит из внешней функции, если лямбда inline).
## Функциональные типы в подробностях
```kotlin
val a: () -> Unit // без параметров, без результата
val b: (Int) -> String // один параметр
val c: (Int, Int) -> Int // два параметра
val d: ((Int) -> Int) -> Int // принимает функцию
val e: (Int) -> ((Int) -> Int) // возвращает функцию
val f: String.(Int) -> String // с получателем (receiver)
val g: ((Int) -> Int)? // nullable
val h: suspend () -> Unit // suspend-функция
```
Тип с получателем вызывается как метод:
```kotlin
val repeatTimes: String.(Int) -> String = { n -> this.repeat(n) }
fun main() {
println("ab".repeatTimes(2)) // abab
}
```
На этом построены `apply`, `buildString` и DSL. Подробнее: [Receivers и context parameters](../../Функциональный%20Kotlin/Receivers%20и%20context%20parameters.md)
## Замыкания
Лямбда захватывает переменные из окружения - **включая изменяемые**:
```kotlin
fun counter(): () -> Int {
var count = 0 // захвачена по ссылке
return { ++count }
}
fun main() {
val next = counter()
println(next()) // 1
println(next()) // 2
println(next()) // 3
}
```
Это важное отличие от Java, где захватить можно только effectively final переменную.
```kotlin
fun main() {
var sum = 0
listOf(1, 2, 3).forEach { sum += it } // в Java так нельзя
println(sum) // 6
}
```
## Вызов
```kotlin
val f: (Int) -> Int = { it + 1 }
fun main() {
println(f(1)) // 2 — обычный вызов
println(f.invoke(1)) // 2 — то же самое
val g: ((Int) -> Int)? = null
println(g?.invoke(1)) // null — безопасный вызов nullable-функции
}
```
## Что происходит под капотом
Функциональный тип компилируется в интерфейс `FunctionN`:
```kotlin
val f: (Int) -> Int = { it * 2 } // объект класса Function1<Int, Int>
```
Это значит аллокацию на каждую лямбду, если она захватывает переменные. Для функций высшего порядка в горячем коде существует `inline` - он подставляет тело лямбды в место вызова, и объекта не создаётся вовсе.
Подробнее: [Inline-функции](../../Функциональный%20Kotlin/Inline-функции.md), [Kotlin в байт-коде](../../Взаимодействие%20с%20Java/Kotlin%20в%20байт-коде.md)
> [!info] Отличия от Java
> | Java | Kotlin |
> |---|---|
> | `x -> x * 2` | `{ x -> x * 2 }` или `{ it * 2 }` |
> | `Function<Integer,Integer>` | `(Int) -> Int` |
> | `BiFunction<A,B,C>` | `(A, B) -> C` |
> | `Supplier<T>` | `() -> T` |
> | `Consumer<T>` | `(T) -> Unit` |
> | `Predicate<T>` | `(T) -> Boolean` |
> | `Runnable` | `() -> Unit` |
> | `String::length` | `String::length` |
> | нет неявного параметра | `it` |
> | захват только effectively final | захват изменяемых переменных |
> | лямбда только для SAM-интерфейса | функциональный тип первого класса |
>
> В Java лямбда - это реализация функционального интерфейса, поэтому для каждой формы нужен свой тип (`Function`, `BiFunction`, `Supplier`, `Consumer`, `Predicate`…). В Kotlin тип функции записывается напрямую: `(Int, String) -> Boolean` не требует придумывать имя.
>
> Захват изменяемых переменных - практическая разница. Java требует `final` или effectively final, вынуждая использовать массив-обёртку `int[] sum = {0}`. В Kotlin `var sum = 0` захватывается как есть (компилятор оборачивает его в `Ref`-объект).
>
> Сравнить: [Функции в Java](../../../Программирование%20на%20Java/Синтаксис%20и%20типы/Работа%20с%20данными%20и%20типами/Функции.md)
> [!warning] Подводные камни
> **`return` в лямбде запрещён** - он относился бы к внешней функции:
> ```kotlin
> fun find(items: List<Int>): Int? {
> items.forEach {
> if (it > 1) return it // выход из find, а не из лямбды!
> } // работает только потому, что forEach inline
> return null
> }
>
> fun main() { println(find(listOf(1, 2, 3))) } // 2
> ```
> Это «нелокальный return», он разрешён только в inline-функциях. Для выхода из самой лямбды нужна метка:
> ```kotlin
> fun main() {
> listOf(1, 2, 3).forEach {
> if (it == 2) return@forEach // пропустить итерацию, как continue
> println(it) // 1, затем 3
> }
> }
> ```
>
> **`it` во вложенных лямбдах затеняется:**
> ```kotlin
> fun main() {
> val nested = listOf(listOf(1, 2))
> // println(nested.map { it.map { it * 2 } }) // непонятно, какое it
> println(nested.map { row -> row.map { v -> v * 2 } }) // [[2, 4]]
> }
> ```
>
> **Каждая не-inline лямбда с захватом - объект.** В цикле на миллион итераций это миллион аллокаций. Стандартные операции (`map`, `filter`, `forEach`) объявлены `inline`, поэтому бесплатны; свои функции высшего порядка в горячем коде тоже стоит делать `inline`.
>
> **Лямбда, захватившая `this`, держит объект в памяти** - классическая утечка в Android, когда колбэк переживает активити.
>
> **Ссылка на перегруженную функцию неоднозначна:**
> ```kotlin
> fun show(x: Int) = "int"
> fun show(x: String) = "str"
>
> fun main() {
> // val f = ::show // ошибка: какая из двух?
> val f: (Int) -> String = ::show // тип разрешает неоднозначность
> println(f(1)) // int
> }
> ```
Дополнительно: [Функции высшего порядка](../../Функциональный%20Kotlin/Функции%20высшего%20порядка.md)
Далее: [Деструктуризация](Деструктуризация.md)
@@ -0,0 +1,249 @@
---
title: Массивы
description: Array, специализированные IntArray, сравнение через contentEquals и почему обычно нужен List
tags:
- kotlin
- stdlib
---
Массив - фиксированная по размеру последовательность элементов. В Kotlin это класс `Array<T>`, а не синтаксическая конструкция.
```kotlin
val a = arrayOf(1, 2, 3)
val b = Array(3) { it * 2 } // [0, 2, 4] — по лямбде от индекса
val c = arrayOfNulls<String>(3) // [null, null, null]
val d = emptyArray<Int>()
println(a[0]) // 1
a[0] = 10
println(a.size) // 3
println(a.joinToString()) // 10, 2, 3
```
Размер задаётся при создании и не меняется. Элементы изменяемы всегда - даже если массив объявлен через `val`.
## Специализированные массивы
Для чисел есть отдельные классы без упаковки:
```kotlin
val ints = intArrayOf(1, 2, 3)
val zeros = IntArray(5) // [0, 0, 0, 0, 0] — заполнен нулями
val squares = IntArray(5) { it * it } // [0, 1, 4, 9, 16]
val doubles = doubleArrayOf(1.0, 2.0)
val chars = charArrayOf('a', 'b')
val bools = BooleanArray(3) // [false, false, false]
```
| Класс | Байт-код | Аналог в Java |
|---|---|---|
| `IntArray` | `int[]` | `int[]` |
| `LongArray` | `long[]` | `long[]` |
| `DoubleArray` | `double[]` | `double[]` |
| `CharArray` | `char[]` | `char[]` |
| `BooleanArray` | `boolean[]` | `boolean[]` |
| `Array<Int>` | `Integer[]` | `Integer[]` |
Разница принципиальна: `Array<Int>` хранит **упакованные** объекты, `IntArray` - примитивы. В горячем коде это заметно по памяти и скорости.
```kotlin
val boxed: Array<Int> = arrayOf(1, 2, 3) // Integer[] — аллокация на элемент
val raw: IntArray = intArrayOf(1, 2, 3) // int[] — без упаковки
println("${boxed.size} ${raw.size}")
```
## Сравнение - только contentEquals
`==` на массивах сравнивает **ссылки**, а не содержимое. Это главная ловушка:
```kotlin
val a = arrayOf(1, 2, 3)
val b = arrayOf(1, 2, 3)
println(a == b) // false — разные объекты!
println(a.contentEquals(b)) // true — так правильно
println(a.contentHashCode()) // хеш по содержимому
```
Для вложенных массивов - глубокие версии:
```kotlin
val x = arrayOf(arrayOf(1), arrayOf(2))
val y = arrayOf(arrayOf(1), arrayOf(2))
println(x.contentEquals(y)) // false — сравнивает ссылки вложенных
println(x.contentDeepEquals(y)) // true
```
## Печать
`toString()` массива бесполезен:
```kotlin
val a = arrayOf(1, 2, 3)
println(a) // [Ljava.lang.Integer;@1b6d3586
println(a.contentToString()) // [1, 2, 3]
println(a.joinToString()) // 1, 2, 3
println("$a".startsWith("[L")) // true — шаблон вызывает тот же toString
```
Внутри строкового шаблона всегда нужен `contentToString()` или `joinToString()`.
## Обход
```kotlin
val a = arrayOf("x", "y", "z")
for (item in a) print(item) // xyz
for (i in a.indices) print(a[i]) // xyz
for ((i, v) in a.withIndex()) print("$i$v ") // 0x 1y 2z
a.forEach { print(it) } // xyz
a.forEachIndexed { i, v -> print("$i$v ") }
println()
println(a.lastIndex) // 2
```
## Операции
Массивы поддерживают те же операции, что коллекции:
```kotlin
val a = intArrayOf(3, 1, 2)
println(a.sorted()) // [1, 2, 3] — возвращает List!
println(a.sortedArray()) // новый IntArray
a.sort() // сортировка на месте
println(a.contentToString()) // [1, 2, 3]
println(a.sum()) // 6
println(a.average()) // 2.0
println(a.max()) // 3
println(a.filter { it > 1 }) // [2, 3] — List
println(a.map { it * 2 }) // [2, 4, 6] — List
println(a.reversed()) // [3, 2, 1] — List
println(a.reversedArray().contentToString()) // [3, 2, 1]
println(a.contains(2)) // true
println(2 in a) // true
println(a.indexOf(2)) // 1
```
Обратите внимание: `filter`, `map`, `sorted`, `reversed` возвращают `List`, а не массив. Версии, сохраняющие тип, имеют суффикс `Array`.
## Копирование и изменение размера
Размер массива неизменен, поэтому «добавление» - это создание нового:
```kotlin
val a = intArrayOf(1, 2, 3)
println((a + 4).contentToString()) // [1, 2, 3, 4] — новый массив
println(a.copyOf().contentToString()) // [1, 2, 3]
println(a.copyOf(5).contentToString()) // [1, 2, 3, 0, 0] — дополнен нулями
println(a.copyOf(2).contentToString()) // [1, 2] — обрезан
println(a.copyOfRange(1, 3).contentToString()) // [2, 3]
println(a.sliceArray(0..1).contentToString()) // [1, 2]
a.fill(0)
println(a.contentToString()) // [0, 0, 0]
```
## Преобразование в коллекции и обратно
```kotlin
val a = arrayOf(1, 2, 3)
val list: List<Int> = a.toList()
val set = a.toSet()
val back: Array<Int> = list.toTypedArray()
val raw: IntArray = list.toIntArray()
println("${list} ${set} ${back.size} ${raw.size}")
```
## Многомерные массивы
Отдельного типа нет - это массив массивов:
```kotlin
val grid = Array(3) { IntArray(4) } // 3x4, заполнен нулями
grid[1][2] = 5
println(grid[1].contentToString()) // [0, 0, 5, 0]
val table = Array(2) { r -> IntArray(2) { c -> r * 2 + c } }
println(table.joinToString { it.contentToString() }) // [0, 1], [2, 3]
```
Строки могут иметь разную длину (рваный массив):
```kotlin
val jagged = arrayOf(intArrayOf(1), intArrayOf(1, 2, 3))
println(jagged.joinToString { it.size.toString() }) // 1, 3
```
## Когда массив, а когда List
Массив нужен редко. Используйте его, если:
- важна производительность и нужны примитивы без упаковки (`IntArray`)
- работаете с API, требующим массив (`vararg`, Java-библиотеки)
- размер фиксирован и известен заранее
В остальных случаях - `List`: он не даёт менять себя случайно, имеет больше операций и корректный `equals`.
```kotlin
val list = listOf(1, 2, 3)
println(list == listOf(1, 2, 3)) // true — работает из коробки
```
Подробнее: [Коллекции](../../Коллекции/Коллекции.md)
## vararg
Параметр с переменным числом аргументов внутри функции - массив:
```kotlin
fun sum(vararg nums: Int): Int {
var total = 0
for (n in nums) total += n // nums это IntArray
return total
}
fun main() {
println(sum(1, 2, 3)) // 6
println(sum()) // 0
val arr = intArrayOf(1, 2)
println(sum(*arr)) // 3 — spread-оператор
}
```
Подробнее: [Функции](Функции.md)
> [!info] Отличия от Java
> | Java | Kotlin |
> |---|---|
> | `int[] a = {1,2,3}` | `val a = intArrayOf(1,2,3)` |
> | `new int[5]` | `IntArray(5)` |
> | `a.length` | `a.size` |
> | `Arrays.toString(a)` | `a.contentToString()` |
> | `Arrays.equals(a,b)` | `a.contentEquals(b)` |
> | `Arrays.deepEquals(a,b)` | `a.contentDeepEquals(b)` |
> | `Arrays.sort(a)` | `a.sort()` |
> | `Arrays.copyOf(a,n)` | `a.copyOf(n)` |
> | `Arrays.fill(a,0)` | `a.fill(0)` |
> | `int[]` и `Integer[]` | `IntArray` и `Array<Int>` |
> | `f(arr)` для varargs | `f(*arr)` — нужен spread |
>
> Статические методы `Arrays` стали методами массива - находятся автодополнением, а не поиском по документации.
>
> Разделение `IntArray` / `Array<Int>` сделано намеренно: в Java `Integer[]` и `int[]` тоже разные, но это легко не заметить. В Kotlin разные имена не позволяют спутать упакованный массив с примитивным.
>
> **Массивы в Kotlin инвариантны**, в Java - ковариантны. Java разрешает `Object[] o = new String[1]; o[0] = 1;` и падает в рантайме (`ArrayStoreException`). В Kotlin `Array<Any> = Array<String>` не скомпилируется. Подробнее: [Вариантность](../../Дженерики/Вариантность.md)
>
> Сравнить: [Массивы в Java](../../../Программирование%20на%20Java/Синтаксис%20и%20типы/Работа%20с%20данными%20и%20типами/Массивы.md)
> [!warning] Подводные камни
> **`==` на массивах сравнивает ссылки.** Самая частая ошибка:
> ```kotlin
> val a = arrayOf(1, 2)
> println(a == arrayOf(1, 2)) // false
> println(a.contentEquals(arrayOf(1, 2))) // true
> ```
> По этой же причине массив нельзя использовать как ключ `Map` или в `Set` - `hashCode` считается по ссылке.
>
> **Массив внутри `data class` ломает `equals`:**
> ```kotlin
> data class Packet(val data: ByteArray)
>
> fun main() {
> val p1 = Packet(byteArrayOf(1))
> println(p1 == Packet(byteArrayOf(1))) // false!
> }
> ```
> Сгенерированный `equals` сравнивает массив по ссылке, а не по содержимому. Поэтому в `data class` с массивом `equals`/`hashCode` пишут руками. Подробнее: [Data class](../Базовые%20типы%20ООП/Data%20class.md)
>
> **`println(array)` печатает мусор** вида `[Ljava.lang.Integer;@1b6d3586`. То же в строковых шаблонах.
>
> **`val` не защищает элементы:**
> ```kotlin
> val a = intArrayOf(1, 2)
> a[0] = 99 // разрешено
> println(a.contentToString()) // [99, 2]
> ```
> Неизменяемость даёт `List`, а не `val`.
>
> **`Array<Int>` вместо `IntArray` - скрытая просадка.** Каждый элемент упакован в `Integer`: больше памяти и промахи кеша.
>
> **`arrayOfNulls` и `Array(n)` для непримитивных типов требуют разного подхода:**
> ```kotlin
> val a = arrayOfNulls<String>(3) // Array<String?>
> val b = Array(3) { "" } // Array<String> — инициализатор обязателен
> println("${a.size} ${b.size}")
> ```
> Создать `Array<String>` без инициализатора нельзя - компилятор не позволит оставить non-null элементы пустыми.
Дополнительно: [Коллекции](../../Коллекции/Коллекции.md)
Далее: [Функции](Функции.md)
@@ -0,0 +1,254 @@
---
title: Приведение типов
description: Проверка is, приведение as и as?, числовые преобразования и стирание типов
tags:
- kotlin
- types
---
## Проверка типа - is
```kotlin
fun describe(x: Any): String = when {
x is String -> "строка длины ${x.length}"
x is Int -> "число ${x + 1}"
x !is Boolean -> "не логическое"
else -> "логическое"
}
fun main() {
println(describe("ab")) // строка длины 2
println(describe(5)) // число 6
println(describe(1.0)) // не логическое
}
```
После успешной проверки компилятор сам приводит тип - это [умное приведение](../Null-безопасность/Умные%20приведения.md), явный каст не нужен.
## Небезопасное приведение - as
```kotlin
fun main() {
val x: Any = "text"
val s = x as String // ok
println(s.length) // 4
val n: Any = 42
// val bad = n as String // ClassCastException
}
```
`as` используют, когда тип известен точно, а компилятор об этом знать не может.
## Безопасное приведение - as?
Возвращает `null` вместо исключения:
```kotlin
fun main() {
val x: Any = "text"
println(x as? Int) // null
println(x as? String) // text
println((x as? Int) ?: 0) // 0 — частая связка с elvis
}
```
Результат всегда nullable, даже при приведении к non-null типу.
Сравнение способов:
| Запись | При несовпадении типа |
|---|---|
| `x is T` | `false` |
| `x as T` | `ClassCastException` |
| `x as? T` | `null` |
## Приведение к nullable
```kotlin
fun main() {
val a: Any? = null
println(a as? String) // null
// println(a as String) // NPE: null cannot be cast to non-null type
println(a as String?) // null — приведение к nullable разрешено
}
```
## Числовые преобразования
Для чисел `as` не применяется - только явные функции:
```kotlin
fun main() {
val i = 42
println(i.toLong()) // 42
println(i.toDouble()) // 42.0
println(i.toByte()) // 42
println(i.toChar()) // *
println(3.99.toInt()) // 3 — обрезание, не округление
println("42".toInt()) // 42
}
```
Неявных преобразований между числовыми типами нет:
```kotlin
fun main() {
val i: Int = 42
// val l: Long = i // ошибка компиляции
val l: Long = i.toLong() // так
}
```
Потеря данных происходит молча:
```kotlin
fun main() {
println(300.toByte()) // 44 — старшие биты отброшены
println(1e20.toInt()) // 2147483647 — обрезано
println(Double.NaN.toInt()) // 0
}
```
Подробнее: [Числа и переполнение](Числа%20и%20переполнение.md)
## Стирание типов и дженерики
Параметры типа стираются при компиляции, поэтому проверить их в рантайме нельзя:
```kotlin
fun check(x: Any) {
if (x is List<*>) { // можно: звёздочка вместо типа
println("список из ${x.size}")
}
// if (x is List<String>) // ошибка: cannot check for instance
// // of erased type
}
fun main() { check(listOf(1, 2)) } // список из 2
```
`List<*>` - это star-проекция: «список чего-то неизвестного». Элементы из него читаются как `Any?`.
Приведение к конкретному дженерику компилируется с предупреждением и небезопасно:
```kotlin
@Suppress("UNCHECKED_CAST")
fun <T> castList(x: Any): List<T> = x as List<T> // unchecked cast
fun main() {
val nums: List<Int> = castList(listOf("a", "b")) // не упадёт здесь!
// println(nums.first() + 1) // ClassCastException только при обращении
println(nums.size) // 2 — работает, типы не проверяются
}
```
Это отложенное падение: ошибка проявится далеко от места приведения.
Обойти стирание можно через `reified` в inline-функциях:
```kotlin
inline fun <reified T> Any.safeCast(): T? = this as? T
fun main() {
println("text".safeCast<String>()) // text
println("text".safeCast<Int>()) // null
}
```
Подробнее: [Reified и inline](../../Дженерики/Reified%20и%20inline.md)
## Приведение в иерархии
```kotlin
open class Animal(val name: String)
class Dog(name: String) : Animal(name) {
fun bark() = "Гав"
}
fun main() {
val a: Animal = Dog("Рекс") // вверх по иерархии — неявно
println(a.name) // Рекс
val d = a as Dog // вниз — только явно
println(d.bark()) // Гав
val other: Animal = Animal("кто-то")
println((other as? Dog)?.bark()) // null — безопасно
}
```
Приведение вверх (к супертипу) всегда безопасно и неявно. Приведение вниз требует `as` или `as?`.
## Коллекции и вариантность
```kotlin
fun main() {
val strings: List<String> = listOf("a")
val anys: List<Any> = strings // ok — List ковариантен по out T
println(anys.size) // 1
val mStrings: MutableList<String> = mutableListOf("a")
// val mAnys: MutableList<Any> = mStrings // ошибка — MutableList инвариантен
}
```
`List<out T>` разрешает такое присваивание, `MutableList<T>` - нет, иначе можно было бы положить `Int` в список строк. Подробнее: [Вариантность](../../Дженерики/Вариантность.md)
## Получение типа в рантайме
```kotlin
fun main() {
val x: Any = "text"
println(x::class) // class kotlin.String
println(x::class.simpleName) // String
println(x::class.java) // class java.lang.String — Java-класс
println(x is CharSequence) // true — проверка по интерфейсу
}
```
Подробнее: [Рефлексия](../../Рефлексия/Рефлексия.md)
> [!info] Отличия от Java
> | Java | Kotlin |
> |---|---|
> | `x instanceof String` | `x is String` |
> | `(String) x` | `x as String` |
> | нет безопасного каста | `x as? String` |
> | `(int) doubleValue` | `doubleValue.toInt()` |
> | `long l = intValue;` неявно | `intValue.toLong()` явно |
> | `x.getClass()` | `x::class` или `x::class.java` |
> | после `instanceof` нужен каст | умное приведение |
> | массивы ковариантны | массивы инвариантны |
>
> Главные приобретения: `as?` вместо пары `instanceof` + каст, и запрет неявных числовых преобразований, из-за которых в Java `long total = intA * intB` тихо переполняется.
>
> Ковариантность массивов в Java - известная дыра в типизации:
> ```java
> Object[] objs = new String[1];
> objs[0] = 42; // компилируется, падает в рантайме
> ```
> В Kotlin `Array<T>` инвариантен, и такой код не соберётся.
>
> Сравнить: [Приведение ссылочных типов в Java](../../../Программирование%20на%20Java/Синтаксис%20и%20типы/Работа%20с%20данными%20и%20типами/Приведение%20ссылочных%20типов.md)
> [!warning] Подводные камни
> **`as` на null даёт NPE, а не ClassCastException:**
> ```kotlin
> fun main() {
> val a: Any? = null
> // println(a as String) // NullPointerException
> println(a as String?) // null — ok
> }
> ```
>
> **`as?` маскирует обе причины.** `null` в результате означает либо неверный тип, либо что значение изначально было `null` - различить невозможно:
> ```kotlin
> fun main() {
> val a: Any? = null
> val b: Any = 42
> println("${a as? String} ${b as? String}") // null null
> }
> ```
>
> **Unchecked cast падает не там, где написан:**
> ```kotlin
> @Suppress("UNCHECKED_CAST")
> fun main() {
> val list = listOf("a") as List<Int> // здесь не падает
> println(list.size) // 1 — и здесь тоже
> // println(list[0] + 1) // ClassCastException только тут
> }
> ```
> Поэтому `@Suppress("UNCHECKED_CAST")` - всегда повод задуматься.
>
> **`toInt()` на `Double` обрезает дробную часть, а не округляет.** `3.99.toInt()` даёт `3`. Для округления - `roundToInt()`.
>
> **`toByte()` и `toShort()` молча теряют старшие биты:** `300.toByte()` возвращает `44`.
>
> **`is` не проверяет параметры дженерика.** `x is List<String>` не скомпилируется, а `x is List<*>` ничего не говорит об элементах. Проверять придётся вручную:
> ```kotlin
> fun main() {
> val x: Any = listOf(1, "a")
> val strings = (x as? List<*>)?.filterIsInstance<String>()
> println(strings) // [a] — только элементы нужного типа
> }
> ```
>
> **`x::class` и `x::class.java` - разные вещи.** Первое - `KClass` Kotlin, второе - `Class` Java. Для `Int` они могут отличаться (`int` против `Integer`).
Дополнительно: [Умные приведения](../Null-безопасность/Умные%20приведения.md)
Далее: [Any, Unit, Nothing](../Базовые%20типы%20ООП/Any,%20Unit,%20Nothing.md)
@@ -0,0 +1,222 @@
---
title: Строки
description: String как неизменяемый тип, полезные методы, StringBuilder и buildString
tags:
- kotlin
- stdlib
---
`String` - неизменяемый тип. Любая «модификация» создаёт новую строку.
```kotlin
val s = "Kotlin"
println(s.length) // 6
println(s[0]) // K — доступ по индексу, как у массива
println(s.uppercase()) // KOTLIN
println(s) // Kotlin — исходная строка не изменилась
```
На JVM это тот же `java.lang.String`, со всем его поведением: пул строк, интернирование, представление в LATIN-1 или UTF-16.
## Сравнение
```kotlin
val a = "Kotlin"
val b = StringBuilder("Kotlin").toString()
println(a == b) // true — сравнение содержимого
println(a === b) // false — разные объекты
println(a.equals("KOTLIN", ignoreCase = true)) // true
```
`==` вызывает `equals()`, поэтому работает правильно всегда. Это главное практическое отличие от Java, где `==` на строках сравнивает ссылки.
Для сортировки и порядка - `compareTo`:
```kotlin
println("abc" < "abd") // true
println("a".compareTo("b")) // -1
println(listOf("b", "a", "c").sorted()) // [a, b, c]
```
## Итерация
Строка - последовательность `Char`:
```kotlin
for (c in "abc") print("$c.") // a.b.c.
println("abc".toList()) // [a, b, c]
println("hello".count { it == 'l' }) // 2
println("hello".filter { it != 'l' }) // heo
println("abc".map { it.uppercaseChar() }) // [A, B, C]
println("abc".reversed()) // cba
```
Работают все операции над коллекциями: `filter`, `map`, `any`, `count`. Только `filter` и `reversed` возвращают `String`, а `map` - `List`.
## Поиск и проверки
```kotlin
val s = "Hello, Kotlin"
println(s.contains("Kotlin")) // true
println("Kotlin" in s) // true — то же самое
println(s.startsWith("Hello")) // true
println(s.endsWith("lin")) // true
println(s.indexOf("o")) // 4 — первое вхождение, -1 если нет
println(s.lastIndexOf("o")) // 8
println(s.isEmpty()) // false — длина 0
println(" ".isBlank()) // true — пусто или только пробелы
println(s.isNotEmpty()) // true
```
Для nullable-строк есть безопасные версии, их можно звать без `?.`:
```kotlin
val n: String? = null
println(n.isNullOrEmpty()) // true
println(n.isNullOrBlank()) // true
println(n.orEmpty()) // "" — пустая строка вместо null
```
## Извлечение частей
```kotlin
val s = "Hello, Kotlin"
println(s.substring(7)) // Kotlin
println(s.substring(0, 5)) // Hello — [0, 5)
println(s.take(5)) // Hello — первые 5
println(s.takeLast(6)) // Kotlin
println(s.drop(7)) // Kotlin — отбросить первые 7
println(s.dropLast(7)) // Hello
println(s.first()) // H
println(s.last()) // n
println(s.getOrNull(100)) // null — вместо исключения
```
`take` и `drop` безопасны: если запросить больше длины, вернётся вся строка, а не исключение. `substring` за границами падает.
```kotlin
println("abc".take(100)) // abc
// println("abc".substring(100)) // StringIndexOutOfBoundsException
```
## Изменение
Функции `capitalize()` и `decapitalize()` устарели и удалены. Замена пишется так:
```kotlin
fun String.capitalize2() =
replaceFirstChar { if (it.isLowerCase()) it.titlecase() else it.toString() }
fun main() { println("hello".capitalize2()) } // Hello
```
Остальные методы возвращают новую строку:
```kotlin
fun String.capitalize2() =
replaceFirstChar { if (it.isLowerCase()) it.titlecase() else it.toString() }
val s = " Hello, Kotlin "
println(s.trim()) // "Hello, Kotlin" — с обеих сторон
println(s.trimStart()) // "Hello, Kotlin "
println(s.trimEnd()) // " Hello, Kotlin"
println("Hello".replace("l", "L")) // HeLLo — все вхождения
println("Hello".replaceFirst("l", "L")) // HeLlo
println("Hello".lowercase()) // hello
println("hello".uppercase()) // HELLO
println("hello".capitalize2()) // Hello
println("ab".repeat(3)) // ababab
println("5".padStart(3, '0')) // 005
println("5".padEnd(3)) // "5 "
```
## Разбиение и сборка
```kotlin
println("a,b,c".split(",")) // [a, b, c]
println("a1b2c".split("1", "2")) // [a, b, c] — несколько разделителей
println("a,b,c".split(",", limit = 2)) // [a, b,c]
println("a\nb".lines()) // [a, b] — по переводам строк
println(listOf("a","b").joinToString("-")) // a-b
println(listOf(1,2,3).joinToString(
separator = ", ", prefix = "[", postfix = "]")) // [1, 2, 3]
println("hello".chunked(2)) // [he, ll, o]
```
`joinToString` - основной способ собрать строку из коллекции, у него есть и `transform`:
```kotlin
println(listOf(1, 2).joinToString { "#$it" }) // #1, #2
```
## Преобразование в число
```kotlin
println("42".toInt()) // 42
println("42".toIntOrNull()) // 42
println("abc".toIntOrNull()) // null — не бросает исключение
println("3.14".toDouble()) // 3.14
println("ff".toInt(16)) // 255 — с основанием
// println("abc".toInt()) // NumberFormatException
```
В коде, который разбирает внешние данные, используйте только версии `*OrNull`.
## StringBuilder и buildString
Для сборки строки в цикле конкатенация создаёт мусор - на каждой итерации новый объект. `StringBuilder` меняет один буфер:
```kotlin
val sb = StringBuilder()
for (i in 1..3) sb.append(i).append(",")
println(sb.toString()) // 1,2,3,
```
Идиоматичнее - `buildString`, он сам создаёт билдер и возвращает результат:
```kotlin
val s = buildString {
append("Привет")
append(", ")
append("Kotlin")
appendLine("!")
append("Строка 2")
}
println(s)
```
Полезные методы билдера: `append`, `appendLine`, `insert(i, x)`, `deleteAt(i)`, `setLength(n)`, `reverse()`, `clear()`.
## Многострочные строки
```kotlin
val json = """
{"name": "Kotlin"}
""".trimIndent()
```
Подробнее: [Строковые шаблоны и вывод](../Базовый%20синтаксис/Строковые%20шаблоны%20и%20вывод.md)
> [!info] Отличия от Java
> | Java | Kotlin |
> |---|---|
> | `s.equals(t)` | `s == t` |
> | `s == t` (ссылки) | `s === t` |
> | `s.charAt(0)` | `s[0]` |
> | `s.isEmpty()` | `s.isEmpty()`, плюс `isBlank()`, `isNullOrBlank()` |
> | `s.toUpperCase()` | `s.uppercase()` |
> | `new StringBuilder()` + `toString()` | `buildString { }` |
> | `String.join(",", list)` | `list.joinToString(",")` |
> | `Integer.parseInt(s)` | `s.toInt()` / `s.toIntOrNull()` |
> | `s.split(",")` → `String[]` | → `List<String>` |
> | `s.substring(100)` падает | `s.take(100)` безопасен |
>
> Главное приобретение - `*OrNull`-функции и безопасные `take`/`drop` вместо исключений на каждой опечатке во входных данных.
>
> `uppercase()` вместо `toUpperCase()` - не просто переименование: старая версия Java зависела от системной локали, из-за чего `"i".toUpperCase()` в турецкой локали давало `İ`. Kotlin по умолчанию использует `Locale.ROOT`.
>
> Сравнить: [Строки в Java](../../../Программирование%20на%20Java/Синтаксис%20и%20типы/Работа%20с%20данными%20и%20типами/Строки.md)
> [!warning] Подводные камни
> **`length` считает кодовые единицы UTF-16, а не символы.** Эмодзи и редкие иероглифы занимают две:
> ```kotlin
> println("😀".length) // 2, а не 1
> println("😀".count()) // 2 — то же самое
> println("😀".codePointCount(0, "😀".length)) // 1 — настоящее число символов
> ```
> Разрезать такую строку по индексу - значит получить битые символы.
>
> **`split` с пустым разделителем и пустой строкой дают неожиданный результат:**
> ```kotlin
> println("".split(",")) // [] — один пустой элемент, размер 1
> println("".split(",").size) // 1
> println("a,,b".split(",")) // [a, , b] — пустой элемент сохраняется
> ```
>
> **`trimIndent` не обрезает пробелы внутри строк** - только общий отступ.
>
> **`replace` принимает строку, а не regex** - в отличие от Java:
> ```kotlin
> println("a.b".replace(".", "-")) // a-b — точка как символ
> println("a.b".replace(Regex("."), "-")) // --- — regex
> ```
> В Java `String.replaceAll` работает с regex, и это частый источник путаницы при переходе.
>
> **Строка в `when` сравнивается по значению, но полнота не проверяется** - для `String` всегда нужен `else`.
Дополнительно: [Char](Char.md)
Далее: [Char](Char.md)
@@ -0,0 +1,280 @@
---
title: Функции
description: Объявление функций, значения по умолчанию, именованные аргументы, vararg и локальные функции
tags:
- kotlin
- syntax
---
Функция объявляется словом `fun`. В Kotlin она может жить на верхнем уровне файла, внутри класса или внутри другой функции.
```kotlin
fun greet(name: String): String {
return "Привет, $name"
}
fun main() {
println(greet("Kotlin")) // Привет, Kotlin
}
```
Порядок: `fun` → имя → параметры → `:` тип результата → тело.
## Выражение вместо тела
Если функция - одно выражение, фигурные скобки и `return` убираются:
```kotlin
fun square(x: Int): Int = x * x
fun greet(name: String) = "Привет, $name" // тип выведен: String
fun max(a: Int, b: Int) = if (a > b) a else b
```
Это основная форма в идиоматичном Kotlin. Тип результата можно опустить - компилятор выведет его. Но для публичного API его лучше писать явно: это контракт, и его случайное изменение не должно быть незаметным.
## Unit - отсутствие результата
```kotlin
fun log(message: String): Unit {
println(message)
}
fun log2(message: String) { // : Unit можно опустить
println(message)
}
```
`Unit` - настоящий тип с единственным значением, а не пустота. Подробнее: [Any, Unit, Nothing](../Базовые%20типы%20ООП/Any,%20Unit,%20Nothing.md)
## Параметры
Параметры всегда **только для чтения** - это `val`, присвоить им нельзя:
```kotlin
fun bad(x: Int) {
// x = 5 // ошибка компиляции
val y = x + 1
println(y)
}
```
Тип параметра обязателен - вывода типов здесь нет.
## Значения по умолчанию
```kotlin
fun connect(host: String, port: Int = 8080, secure: Boolean = false) {
println("$host:$port secure=$secure")
}
fun main() {
connect("localhost") // localhost:8080 secure=false
connect("localhost", 443) // localhost:443 secure=false
connect("localhost", 443, true) // localhost:443 secure=true
}
```
Это заменяет перегрузки: вместо трёх версий метода - одна функция. Значением по умолчанию может быть любое выражение, включая вызов функции и другие параметры:
```kotlin
fun repeat2(text: String, times: Int = text.length) = text.repeat(times)
fun main() {
println(repeat2("ab")) // abab — times = 2
println(repeat2("ab", 3)) // ababab
}
```
Выражение вычисляется при каждом вызове, в котором аргумент опущен.
## Именованные аргументы
Аргумент можно передать по имени параметра:
```kotlin
fun connect(host: String, port: Int = 80, secure: Boolean = false, timeout: Int = 30) {
println("$host:$port s=$secure t=$timeout")
}
fun main() {
connect("localhost", secure = true) // пропустили port
connect(host = "localhost", timeout = 60)
connect(port = 443, host = "localhost") // порядок не важен
}
```
Две причины использовать:
1. **Пропустить** часть параметров со значениями по умолчанию
2. **Читаемость** на вызовах с непонятными литералами
```kotlin
// что здесь true?
// createUser("Иван", true, false, true)
// так понятно:
// createUser("Иван", admin = true, banned = false, verified = true)
```
Правило: позиционные аргументы идут до именованных. После первого именованного все следующие тоже должны быть именованными - кроме `vararg` со spread.
## vararg
Переменное число аргументов:
```kotlin
fun sum(vararg nums: Int): Int {
var total = 0
for (n in nums) total += n // nums имеет тип IntArray
return total
}
fun main() {
println(sum(1, 2, 3)) // 6
println(sum()) // 0
val arr = intArrayOf(1, 2, 3)
println(sum(*arr)) // 6 — spread-оператор обязателен
}
```
Правила:
- `vararg` может быть **только один** в функции
- он не обязан быть последним, но тогда остальные передаются по имени
- внутри функции это массив (`Array<T>` или специализированный)
- для передачи готового массива нужен `*` (spread)
```kotlin
fun tag(vararg classes: String, id: String) = "$id: ${classes.joinToString()}"
fun main() {
println(tag("a", "b", id = "x")) // x: a, b
}
```
## Локальные функции
Функция внутри функции - видит переменные внешней:
```kotlin
fun process(items: List<Int>): List<String> {
val prefix = ">> " // захватывается
fun format(x: Int): String = "$prefix$x" // локальная функция
return items.map { format(it) }
}
fun main() {
println(process(listOf(1, 2))) // [>> 1, >> 2]
}
```
Нужны, когда логика используется только внутри одной функции и не стоит того, чтобы выносить её в класс или в приватную функцию файла.
## Перегрузка
Функции с одним именем и разными параметрами:
```kotlin
fun show(x: Int) = "число $x"
fun show(x: String) = "строка $x"
fun show(x: Int, y: Int) = "пара $x,$y"
fun main() {
println(show(1)) // число 1
println(show("a")) // строка a
println(show(1, 2)) // пара 1,2
}
```
Сигнатуру образуют имя и типы параметров. Тип результата в неё **не входит** - две функции, отличающиеся только им, не скомпилируются.
Чаще всего перегрузка не нужна: её заменяют значения по умолчанию.
## Передача аргументов
Аргументы передаются **по значению**, как в Java:
- примитив - копируется значение
- объект - копируется ссылка
Значит, переприсвоить параметр внутри функции нельзя (он `val`), но изменяемый объект изменить можно:
```kotlin
fun addItem(list: MutableList<Int>) {
list.add(99) // объект изменён — виден снаружи
}
fun main() {
val items = mutableListOf(1)
addItem(items)
println(items) // [1, 99]
}
```
## Однострочные функции-расширения
Функции можно добавлять к существующим типам:
```kotlin
fun String.shout() = uppercase() + "!"
fun main() {
println("привет".shout()) // ПРИВЕТ!
}
```
Подробнее: [Функции-расширения](../../Функциональный%20Kotlin/Функции-расширения.md)
## Функции как значения
Функция может быть параметром и результатом:
```kotlin
fun apply2(x: Int, f: (Int) -> Int) = f(x)
fun main() {
println(apply2(3) { it * 2 }) // 6
println(apply2(3, ::square)) // 9 — ссылка на функцию
}
fun square(x: Int) = x * x
```
Подробнее: [Лямбды и функциональные типы](Лямбды%20и%20функциональные%20типы.md), [Функции высшего порядка](../../Функциональный%20Kotlin/Функции%20высшего%20порядка.md)
> [!info] Отличия от Java
> | Java | Kotlin |
> |---|---|
> | метод только внутри класса | функция на верхнем уровне |
> | `int f(int x) { return x; }` | `fun f(x: Int) = x` |
> | `void` | `Unit` (можно опустить) |
> | нет значений по умолчанию | `fun f(x: Int = 0)` |
> | нет именованных аргументов | `f(x = 1)` |
> | `int... nums` | `vararg nums: Int` |
> | `f(arr)` для varargs | `f(*arr)` |
> | параметр можно переприсвоить | параметр - `val` |
> | перегрузки для опциональных параметров | одна функция со значениями по умолчанию |
>
> Значения по умолчанию + именованные аргументы убирают главный источник шума в Java API - «телескопические» перегрузки:
> ```java
> void connect(String host) { connect(host, 8080); }
> void connect(String host, int port) { connect(host, port, false); }
> void connect(String host, int port, boolean secure) { ... }
> ```
> В Kotlin это одна строка. Для вызова из Java есть `@JvmOverloads`, который генерирует такие перегрузки автоматически - см. [Kotlin из Java](../../Взаимодействие%20с%20Java/Kotlin%20из%20Java.md).
>
> Сравнить: [Функции в Java](../../../Программирование%20на%20Java/Синтаксис%20и%20типы/Работа%20с%20данными%20и%20типами/Функции.md)
> [!warning] Подводные камни
> **Изменяемое значение по умолчанию вычисляется на каждом вызове** - в отличие от Python, где оно создаётся один раз. Это как раз правильное поведение:
> ```kotlin
> fun add(item: Int, list: MutableList<Int> = mutableListOf()): List<Int> {
> list.add(item)
> return list
> }
>
> fun main() {
> println(add(1)) // [1]
> println(add(2)) // [2] — новый список, а не [1, 2]
> }
> ```
>
> **Значения по умолчанию не видны из Java.** Без `@JvmOverloads` Java-код обязан передавать все аргументы.
>
> **Переопределённый метод не может изменить значения по умолчанию:**
> ```kotlin
> open class A { open fun f(x: Int = 1) = x }
> class B : A() {
> override fun f(x: Int) = x * 2 // значение по умолчанию наследуется,
> } // переобъявить его нельзя
> ```
>
> **`vararg` без spread принимает массив как один элемент:**
> ```kotlin
> fun show(vararg items: Any) = items.size
>
> fun main() {
> val arr = arrayOf(1, 2, 3)
> println(show(arr)) // 1 — массив целиком как один аргумент
> println(show(*arr)) // 3 — распакован
> }
> ```
>
> **Перегрузка + значения по умолчанию = неоднозначность.** Компилятор выберет наиболее специфичную перегрузку, а не ту, что со значениями по умолчанию:
> ```kotlin
> fun f(x: Int, y: Int = 0) = "два"
> fun f(x: Int) = "один"
>
> fun main() {
> println(f(1)) // "один" — перегрузка без default побеждает
> }
> ```
> Такого сочетания лучше избегать.
Дополнительно: [Лямбды и функциональные типы](Лямбды%20и%20функциональные%20типы.md)
Далее: [Лямбды и функциональные типы](Лямбды%20и%20функциональные%20типы.md)
@@ -0,0 +1,231 @@
---
title: Числа и переполнение
description: Переполнение целых, точность Double, BigDecimal для денег и математические функции
tags:
- kotlin
- stdlib
---
## Переполнение целых
Kotlin, как и Java, **не проверяет переполнение**. При выходе за границы значение «заворачивается»:
```kotlin
println(Int.MAX_VALUE) // 2147483647
println(Int.MAX_VALUE + 1) // -2147483648 — тихо
println(Int.MIN_VALUE - 1) // 2147483647
```
Это не исключение и даже не предупреждение - просто неверный результат.
Границы типов:
```kotlin
println("${Byte.MIN_VALUE}..${Byte.MAX_VALUE}") // -128..127
println("${Int.MIN_VALUE}..${Int.MAX_VALUE}") // -2147483648..2147483647
println("${Long.MIN_VALUE}..${Long.MAX_VALUE}") // -9223372036854775808..9223372036854775807
```
### Где это кусает
Самый частый случай - умножение `Int`, результат которого не влезает:
```kotlin
val ms = 1000 * 60 * 60 * 24 * 30 // -1702967296 — переполнение!
val msOk = 1000L * 60 * 60 * 24 * 30 // 2592000000 — первый операнд Long
println("$ms $msOk")
```
Правило: если промежуточный результат может превысить 2 млрд, делайте `Long` **первый** операнд. Приписать `L` к последнему множителю недостаточно - умножение идёт слева направо.
### Проверяемая арифметика
Явные функции, бросающие исключение при переполнении:
```kotlin
println(Math.addExact(1, 2)) // 3
// println(Math.addExact(Int.MAX_VALUE, 1)) // ArithmeticException
// println(Math.multiplyExact(100000, 100000)) // ArithmeticException
```
Доступны `addExact`, `subtractExact`, `multiplyExact`, `incrementExact`, `toIntExact`. Это функции JDK, в stdlib Kotlin аналогов нет.
## Деление
```kotlin
println(7 / 2) // 3 — целочисленное, дробная часть отброшена
println(-7 / 2) // -3 — округление к нулю, не к меньшему
println(7 % 2) // 1
println(-7 % 2) // -1 — знак от делимого
println(7.0 / 2) // 3.5
```
Деление на целочисленный ноль - исключение, на вещественный - нет:
```kotlin
// println(1 / 0) // ArithmeticException: / by zero
println(1.0 / 0) // Infinity
println(-1.0 / 0) // -Infinity
println(0.0 / 0) // NaN
```
### Остаток для отрицательных
`%` в Kotlin даёт остаток со знаком делимого, что ломает индексацию по кругу:
```kotlin
println(-1 % 3) // -1 — не 2!
println((-1).mod(3)) // 2 — математический модуль
println((-1).rem(3)) // -1 — то же, что %
```
Для циклических индексов нужен `mod`, а не `%`.
## Вещественные числа
`Float` и `Double` следуют IEEE 754 и **не представляют десятичные дроби точно**:
```kotlin
println(0.1 + 0.2) // 0.30000000000000004
println(0.1 + 0.2 == 0.3) // false
println(1.0 - 0.9) // 0.09999999999999998
```
Сравнивать нужно с допуском:
```kotlin
fun almostEqual(a: Double, b: Double, eps: Double = 1e-9) =
kotlin.math.abs(a - b) < eps
println(almostEqual(0.1 + 0.2, 0.3)) // true
```
### NaN и бесконечности
```kotlin
val nan = Double.NaN
println(nan == nan) // false! NaN не равен ничему, включая себя
println(nan.isNaN()) // true — единственный верный способ проверки
println(Double.POSITIVE_INFINITY) // Infinity
println(1.0.isFinite()) // true
println(0.0 == -0.0) // true
```
`NaN` ведёт себя по-разному в операторах и в коллекциях - это редкая, но болезненная ловушка:
```kotlin
println(Double.NaN == Double.NaN) // false — оператор
println(Double.NaN.equals(Double.NaN)) // true — метод equals
println(listOf(Double.NaN).contains(Double.NaN)) // true — через equals
```
## Деньги - BigDecimal
Для денег `Double` использовать нельзя: ошибки округления накапливаются.
```kotlin
import java.math.BigDecimal
import java.math.RoundingMode
fun main() {
val a = BigDecimal("0.1") // только из строки!
val b = BigDecimal("0.2")
println(a + b) // 0.3 — точно
println(a.add(b) == BigDecimal("0.3")) // true
val price = BigDecimal("100.555")
println(price.setScale(2, RoundingMode.HALF_UP)) // 100.56
// операторы перегружены, работают привычно
println(BigDecimal("10") * BigDecimal("3")) // 30
println(BigDecimal("10").divide(BigDecimal("4"))) // 2.5
}
```
Создавать **только из строки**: `BigDecimal(0.1)` подхватит уже испорченное значение `Double`:
```kotlin
import java.math.BigDecimal
fun main() {
println(BigDecimal(0.1)) // 0.1000000000000000055511151231257827...
println(BigDecimal("0.1")) // 0.1
}
```
`BigDecimal.equals` сравнивает и масштаб, поэтому `2.0` и `2.00` не равны:
```kotlin
import java.math.BigDecimal
fun main() {
println(BigDecimal("2.0") == BigDecimal("2.00")) // false
println(BigDecimal("2.0").compareTo(BigDecimal("2.00")) == 0) // true
}
```
Для сравнения значений - `compareTo`, не `==`.
## Математические функции
```kotlin
import kotlin.math.*
fun main() {
println(abs(-5)) // 5
println(max(3, 7)) // 7
println(min(3, 7)) // 3
println(sqrt(16.0)) // 4.0
println(2.0.pow(10)) // 1024.0
println(PI) // 3.141592653589793
println(round(2.5)) // 2.0 — не 3.0! округление к чётному
println(round(3.5)) // 4.0
println(floor(2.9)) // 2.0
println(ceil(2.1)) // 3.0
println(truncate(2.9)) // 2.0
println(2.567.roundToInt()) // 3
println(hypot(3.0, 4.0)) // 5.0
}
```
Пакет `kotlin.math` нужно импортировать - автоматически он не подключается.
Обратите внимание на `round`: при ровной половине он округляет **к ближайшему чётному** (IEEE 754 round-half-even), поэтому `round(2.5)` даёт `2.0`, а `round(3.5)` - `4.0`. Если нужно привычное «половина вверх», берите `roundToInt()`: `2.5.roundToInt()` даёт `3`.
### Ограничение диапазона
```kotlin
println(15.coerceIn(0, 10)) // 10
println((-5).coerceIn(0, 10)) // 0
println(5.coerceAtLeast(0)) // 5
println(15.coerceAtMost(10)) // 10
```
Удобнее, чем `max(0, min(10, x))`.
## Преобразования и потеря данных
```kotlin
import kotlin.math.roundToInt
fun main() {
println(300.toByte()) // 44 — старшие биты отброшены
println(3.99.toInt()) // 3 — отбрасывание, не округление
println((-3.99).toInt()) // -3
println(3.99.roundToInt()) // 4 — настоящее округление
println(Double.NaN.toInt()) // 0 — тихо!
println(1e20.toInt()) // 2147483647 — обрезано до MAX_VALUE
}
```
`toInt()` на `Double` никогда не падает - он тихо даёт неверный результат. Для контроля используйте `roundToInt()` и проверку `isFinite()`.
## Представление и биты
```kotlin
println(255.toString(2)) // 11111111 — в двоичной
println(255.toString(16)) // ff
println("ff".toInt(16)) // 255
println(5.countOneBits()) // 2 — число единичных битов
println(Integer.toBinaryString(-1)) // 32 единицы
```
> [!info] Отличия от Java
> | Java | Kotlin |
> |---|---|
> | `Math.abs(x)` | `kotlin.math.abs(x)` |
> | `Math.pow(2, 10)` | `2.0.pow(10)` |
> | `Integer.MAX_VALUE` | `Int.MAX_VALUE` |
> | `Integer.parseInt(s)` | `s.toInt()` |
> | `Integer.toBinaryString(n)` | `n.toString(2)` |
> | `Math.floorMod(-1, 3)` | `(-1).mod(3)` |
> | неявное `int` → `long` | только явный `toLong()` |
> | `Math.addExact` | то же (из JDK) |
>
> Поведение переполнения, деления и IEEE 754 полностью унаследовано от JVM - здесь Kotlin ничего не меняет и не защищает. Выигрыш только в читаемости: `2.0.pow(10)` вместо `Math.pow(2, 10)`, `coerceIn` вместо вложенных `min`/`max`.
>
> Запрет неявных преобразований - реальная защита: в Java `long total = intA * intB;` переполняется до присваивания, и компилятор молчит. В Kotlin такой код не соберётся без явного `toLong()`.
>
> Сравнить: [Типы данных в Java](../../../Программирование%20на%20Java/Синтаксис%20и%20типы/Базовый%20синтаксис/Типы%20данных.md)
> [!warning] Подводные камни
> **`1000 * 60 * 60 * 24 * 30` переполняется** - самая частая реальная ошибка с числами. Все операнды `Int`, результат считается в `Int`, и только потом присваивается в `Long`.
>
> **`Double` для денег даёт расхождения в копейках**, которые накапливаются на больших объёмах. Только `BigDecimal` из строки или целые копейки в `Long`.
>
> **`NaN == NaN` ложно, а `listOf(NaN).contains(NaN)` истинно.** Сортировка и `distinct()` с `NaN` ведут себя неинтуитивно.
>
> **`toInt()` на `Double` обрезает, а `NaN` превращает в 0** - без единого предупреждения.
>
> **`%` для отрицательных чисел даёт отрицательный остаток.** Код `arr[(i - 1) % n]` упадёт на `i = 0`; нужен `arr[(i - 1).mod(n)]`.
>
> **`BigDecimal("2.0") != BigDecimal("2.00")`** - `equals` учитывает масштаб. Сравнивайте через `compareTo`.
Дополнительно: [Типы данных](../Базовый%20синтаксис/Типы%20данных.md)
Далее: [Массивы](Массивы.md)