Kotlin guide done. (20.10.26)
This commit is contained in:
@@ -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)
|
||||
Reference in New Issue
Block a user