--- 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`) | **нет** - дженерики стираются | | поле дженерик-класса | **нет** | ```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 = listOf(UserId(1)) // объекты внутри списка println("${a.value} ${b?.value} ${c.size}") } ``` То есть `List` по памяти не лучше `List`. Выгода - в типобезопасности, а не в аллокациях. ## 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` хранит объекты из-за стирания типов. > > **`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)