KotlinMiddleТеория
Что такое value classes (ранее inline classes) в Kotlin?
Сначала попробуйте ответить сами30 секунд
Value class — обёртка над одним значением, которую JVM заменяет примитивом напрямую (без heap-аллокации) в большинстве случаев. Даёт типобезопасность без накладных расходов.
Объявление
@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 class
Value class (раньше назывался inline class) — класс, аннотированный @JvmInline и объявленный с ключевым словом value. Он оборачивает ровно одно неизменяемое свойство. Компилятор Kotlin заменяет экземпляры value class на сам тип обёртываемого значения везде, где это возможно, избегая создания объекта на куче.
Зачем нужны 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-значение).
Частые вопросы на собеседовании
Типичные ошибки
- Объяснять «value classes» только как синтаксис и не описывать поведение runtime/compiler.
- Игнорировать важный риск: Нельзя обещать нулевой overhead всегда: boxing появляется на generic границах, nullable значениях и интерфейсах.
- Давать пример без edge case: отмены, null, recomposition, platform boundary или ошибки.
Что проверяет интервьюер
- Формулирует суть темы «value classes» своими словами и связывает ее с кодом.
- Называет механизм: Она объявляется как @JvmInline value class с единственным primary constructor property и имеет ограничения на identity и inheritance.
- Видит production-последствие: Нельзя обещать нулевой overhead всегда: boxing появляется на generic границах, nullable значениях и интерфейсах.