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 значениях и интерфейсах.

Sources

Related topics