11 KiB
title, description, tags
| title | description | tags | |||
|---|---|---|---|---|---|
| Value class | Обёртка без аллокации, типобезопасность примитивов, mangling имён и границы инлайнинга |
|
value class - обёртка над одним значением, которая в байт-коде обычно исчезает: вместо объекта остаётся само значение.
@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 - без неё компилятор выдаст ошибку.
Зачем
Основная причина - типобезопасность без накладных расходов. Классическая проблема:
fun transfer(from: Int, to: Int, amount: Int) = "$from -> $to: $amount"
fun main() {
println(transfer(100, 1, 2)) // перепутали порядок — компилятор молчит
}
Все параметры Int, поэтому их легко перепутать. С value class такая ошибка становится ошибкой компиляции:
@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.
Что можно внутри
@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.
// @JvmInline value class Bad(val a: Int, val b: Int) // ошибка: только одно поле
// @JvmInline value class Bad2(var a: Int) // ошибка: нужен val
Реализация интерфейса
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>) |
нет - дженерики стираются |
| поле дженерик-класса | нет |
@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-сигнатуры, компилятор добавляет суффикс к имени метода:
@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:
@JvmInline value class AccountId(val value: Int)
@JvmName("processAccount")
fun process(id: AccountId) = id.value
fun main() { println(process(AccountId(1))) } // 1
Подробнее: Kotlin из Java
Равенство
Сравнение идёт по содержимому, причём без упаковки:
@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
@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 тоже работает, но создаёт объект:
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 - не замена
typealias UserId2 = Int
fun load(id: UserId2) = id
fun main() {
println(load(42)) // 42 — любое Int подходит, защиты нет
}
typealias - только псевдоним, он не создаёт новый тип и не защищает от путаницы. Подробнее: Typealias
[!info] Отличия от 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 classKotlin.
Java Kotlin класс-обёртка + аллокация @JvmInline value classintнапрямуюvalue classс тем же байт-кодомValhalla (в разработке) работает с Kotlin 1.5 Сравнить: Классы-обертки в Java
[!warning] Подводные камни
@JvmInlineобязательна. Без неё на JVM компилятор выдаст ошибку, а не предупреждение:// 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?- настоящий объект:@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:@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обойдёт проверку типов:@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 Далее: Object и companion object