Kotlin guide done. (20.10.26)

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