KotlinMiddleTechnical
Что такое value classes (ранее inline classes) в Kotlin?
Value class — обёртка над одним значением, которую JVM заменяет примитивом напрямую (без heap-аллокации) в большинстве случаев. Даёт типобезопасность без накладных расходов.
Что такое value class
Value class (раньше назывался inline class) — класс, аннотированный @JvmInline и объявленный с ключевым словом value. Он оборачивает ровно одно неизменяемое свойство. Компилятор Kotlin заменяет экземпляры value class на сам тип обёртываемого значения везде, где это возможно, избегая создания объекта на куче.
Объявление
@JvmInline
value class Email(val address: String) {
init {
require(address.contains('@')) { "Invalid email: $address" }
}
fun domain(): String = address.substringAfter('@')
}
@JvmInline
value class UserId(val id: Long)
fun sendEmail(email: Email, sender: UserId) {
println("Sending to ${email.address} from user ${sender.id}")
}
fun main() {
val email = Email("alice@example.com")
val userId = UserId(42L)
sendEmail(email, userId) // в байткоде: sendEmail(String, long)
println(email.domain()) // example.com
}
Зачем нужны value classes
- Типобезопасность: нельзя случайно передать
UserIdтуда, где ожидаетсяOrderId, даже если оба —Long. - Производительность: JVM обрабатывает
EmailкакString— нет аллокации обёртки. - Валидация:
init-блок выполняется при создании, даже в unboxed-режиме. - Методы: можно добавлять методы и реализовывать интерфейсы.
Реализация интерфейсов
interface Printable {
fun print()
}
@JvmInline
value class Name(val value: String) : Printable {
override fun print() = println("Name: $value")
}
fun printAll(items: List<Printable>) {
items.forEach { it.print() } // здесь происходит boxing!
}
Когда происходит boxing
- Generic-позиции:
List<Email>,Map<UserId, Email> - Nullable:
Email? - Вызов через интерфейс
- Рефлексия
Ограничения
- Ровно одно свойство в primary constructor, только
val. - Нельзя наследоваться от других классов (кроме
Any). - Нет backing fields кроме единственного свойства.
- Не поддерживает делегирование свойств внутри класса.
Подводные камни
- Забывают
@JvmInline— без неёvalue classне компилируется на JVM-таргете. - Nullable value class всегда boxed — использование
UserId?повсюду сводит на нет оптимизацию. - Java-код видит метод с суффиксом
-implи-DefaultConstructorMarker— прямой вызов из Java требует специального companion-метода. - В generic-коллекциях (
listOf(Email("a@b.c"))) boxing всё равно происходит — аллокация не устранена. - Equals и hashCode делегируют обёртываемому типу — два разных value class над
Longбудут равны, еслиLong-значения равны (при boxing). init-блок вызывается в конструкторе, но JVM может соптимизировать unboxed-путь без вызова конструктора — проверяйте байткод при критичной валидации.- Serialization: kotlinx.serialization требует явного
@Serializable, а JSON-формат может отличаться от ожидаемого (обёртка vs raw-значение).
Common mistakes
- Объяснять «value classes» только как синтаксис и не описывать поведение runtime/compiler.
- Игнорировать важный риск: Нельзя обещать нулевой overhead всегда: boxing появляется на generic границах, nullable значениях и интерфейсах.
- Давать пример без edge case: отмены, null, recomposition, platform boundary или ошибки.
What the interviewer is testing
- Формулирует суть темы «value classes» своими словами и связывает ее с кодом.
- Называет механизм: Она объявляется как @JvmInline value class с единственным primary constructor property и имеет ограничения на identity и inheritance.
- Видит production-последствие: Нельзя обещать нулевой overhead всегда: boxing появляется на generic границах, nullable значениях и интерфейсах.