Quantas vezes você já passou um String onde deveria ser um ID, ou confundiu metros com quilômetros porque ambos eram Double? Esses bugs silenciosos são comuns em projetos Kotlin — e as value classes existem justamente para eliminá-los. Neste guia, vamos explorar tudo sobre value classes: desde a sintaxe básica até padrões avançados de modelagem de domínio, sempre com foco em performance e type safety.

O que São Value Classes?

Value classes (anteriormente chamadas de inline classes) são um recurso do Kotlin que permite criar tipos wrapper sem custo de alocação em runtime. O compilador substitui a value class pelo valor interno sempre que possível, eliminando a criação de objetos no heap.

@JvmInline
value class UserId(val value: Long)

@JvmInline
value class Email(val value: String)

fun findUser(id: UserId): User? { /* ... */ }

// Em tempo de compilação: tipagem forte
// Em runtime: é apenas um Long (sem boxing)
val id = UserId(42)
findUser(id) // ✅ Compila
findUser(UserId(99)) // ✅ Compila
// findUser(42) // ❌ Não compila — type safety!

A anotação @JvmInline é obrigatória desde o Kotlin 1.5 e indica ao compilador que esta classe deve ser “inlineada” — ou seja, o wrapper é removido no bytecode gerado.

Evolução: De Inline Classes para Value Classes

As value classes são a evolução das inline classes experimentais do Kotlin 1.2. A mudança de nome reflete a intenção do time do Kotlin de, no futuro, suportar value classes com múltiplas propriedades (Valhalla Project da JVM). Por enquanto, a restrição é:

  • Exatamente uma propriedade no construtor primário
  • A propriedade deve ser val (imutável)
  • Anotação @JvmInline obrigatória para a JVM
// ✅ Válido
@JvmInline
value class ProductName(val name: String)

// ❌ Inválido — duas propriedades
// @JvmInline
// value class Money(val amount: Double, val currency: String)

Casos de Uso Práticos

1. Typed IDs (IDs Tipados)

O caso de uso mais clássico. Em vez de passar Long ou String para tudo, cada entidade tem seu próprio tipo de ID:

@JvmInline
value class UserId(val value: Long)

@JvmInline
value class OrderId(val value: Long)

@JvmInline
value class ProductId(val value: Long)

// Impossível confundir um ID com outro
fun getOrder(orderId: OrderId): Order? { /* ... */ }
fun getUser(userId: UserId): User? { /* ... */ }

val orderId = OrderId(100)
val userId = UserId(200)

getOrder(orderId) // ✅
// getOrder(userId) // ❌ Type mismatch — erro em tempo de compilação!

Esse padrão é especialmente poderoso em projetos com arquitetura limpa onde a camada de domínio precisa de tipos expressivos.

2. Unidades de Medida

Evite confusões entre unidades usando value classes:

@JvmInline
value class Meters(val value: Double) {
    fun toKilometers(): Kilometers = Kilometers(value / 1000.0)
    operator fun plus(other: Meters): Meters = Meters(value + other.value)
    operator fun compareTo(other: Meters): Int = value.compareTo(other.value)
}

@JvmInline
value class Kilometers(val value: Double) {
    fun toMeters(): Meters = Meters(value * 1000.0)
}

@JvmInline
value class Celsius(val value: Double) {
    fun toFahrenheit(): Fahrenheit = Fahrenheit(value * 9.0 / 5.0 + 32.0)
}

@JvmInline
value class Fahrenheit(val value: Double)

// Uso
val distance = Meters(1500.0)
val km = distance.toKilometers()
println("${distance.value}m = ${km.value}km") // 1500.0m = 1.5km

3. Validação no Construtor

Combine value classes com blocos init para garantir invariantes:

@JvmInline
value class Email(val value: String) {
    init {
        require(value.contains("@") && value.contains(".")) {
            "Email inválido: $value"
        }
    }
}

@JvmInline
value class Percentage(val value: Double) {
    init {
        require(value in 0.0..100.0) {
            "Porcentagem deve estar entre 0 e 100: $value"
        }
    }
}

@JvmInline
value class NonBlankString(val value: String) {
    init {
        require(value.isNotBlank()) { "String não pode ser vazia" }
    }
}

// Falha rápido — melhor que descobrir o bug em produção
val email = Email("dev@kotlin.dev.br") // ✅
// val invalid = Email("sem-arroba") // ❌ IllegalArgumentException

4. Wrapper para APIs Externas

Torne APIs de terceiros mais seguras com tipagem:

@JvmInline
value class JsonString(val raw: String)

@JvmInline
value class BearerToken(val token: String) {
    fun toHeader(): String = "Bearer $token"
}

@JvmInline
value class Url(val value: String) {
    init {
        require(value.startsWith("http://") || value.startsWith("https://")) {
            "URL inválida: $value"
        }
    }
}

fun callApi(url: Url, token: BearerToken): JsonString {
    // Impossível passar token onde deveria ser URL e vice-versa
    // ...
    return JsonString("""{"status": "ok"}""")
}

Performance: Value Classes vs Data Classes

A principal vantagem de value classes sobre data classes é a ausência de alocação no heap na maioria dos cenários:

// Data class — SEMPRE aloca um objeto no heap
data class UserIdData(val value: Long)

// Value class — NÃO aloca objeto (inlined como Long)
@JvmInline
value class UserIdValue(val value: Long)

fun processDataId(id: UserIdData) = id.value * 2
fun processValueId(id: UserIdValue) = id.value * 2

// No bytecode:
// processDataId recebe um objeto UserIdData
// processValueId recebe um long primitivo

Quando Ocorre Boxing?

Value classes são alocadas no heap (boxing) em cenários específicos:

@JvmInline
value class Name(val value: String)

// ❌ Boxing ocorre aqui:
val nullable: Name? = Name("Kotlin")       // Nullable → boxing
val list: List<Name> = listOf(Name("A"))   // Generic → boxing
val any: Any = Name("B")                   // Upcasting → boxing

// ✅ Sem boxing aqui:
val name: Name = Name("Kotlin")            // Uso direto → inlined
fun greet(name: Name) = "Olá, ${name.value}" // Parâmetro → inlined

Na prática, para a maioria dos casos de uso (parâmetros de função, variáveis locais, retornos), o compilador elimina a alocação. O boxing acontece apenas quando o tipo precisa ser “apagado” por nullability ou generics.

Value Classes e Interfaces

Value classes podem implementar interfaces, o que abre possibilidades interessantes:

interface Identifiable {
    val id: String
}

@JvmInline
value class SkuCode(val code: String) : Identifiable {
    override val id: String get() = code
}

@JvmInline
value class Barcode(val code: String) : Identifiable {
    override val id: String get() = code
}

fun logIdentifiable(item: Identifiable) {
    println("Processing: ${item.id}")
}

Atenção: quando uma value class é usada como interface (upcast), ocorre boxing. Use interfaces com value classes conscientemente.

Limitações

Value classes têm algumas restrições importantes:

  1. Apenas uma propriedade no construtor primário (por enquanto)
  2. Não podem ter lateinit ou propriedades delegadas
  3. Não podem participar de hierarquias de classe — não podem ser open, abstract ou sealed
  4. Boxing em contextos genéricos e nullable
  5. Não podem ter backing fields além da propriedade primária

Se você precisa de mais de uma propriedade, use uma data class ou aguarde futuras versões do Kotlin com suporte a multi-property value classes.

Para representar estados complexos com restrições de tipo, considere combinar value classes com sealed classes:

@JvmInline
value class OrderId(val value: Long)

sealed interface OrderStatus {
    data class Pending(val orderId: OrderId) : OrderStatus
    data class Shipped(val orderId: OrderId, val trackingCode: String) : OrderStatus
    data class Delivered(val orderId: OrderId) : OrderStatus
}

Comparação Rápida

AspectoValue ClassData ClassType Alias
Type safety✅ Tipo distinto✅ Tipo distinto❌ Mesmo tipo
Alocação heap❌ Geralmente não✅ SempreN/A
Múltiplas props❌ Apenas uma✅ SimN/A
equals/hashCode✅ Automático✅ AutomáticoN/A
copy()❌ Não✅ SimN/A

Boas Práticas

  1. Use value classes para IDs e identificadores — é o caso de uso com melhor relação custo-benefício
  2. Adicione validação no init — falhe rápido em vez de propagar dados inválidos
  3. Prefira value classes a type aliases quando precisar de type safety real
  4. Evite em contextos genéricos se performance for crítica (listas, maps, nullable)
  5. Combine com sealed classes para modelagem de domínio expressiva
  6. Documente o comportamento de boxing para o time entender quando a alocação acontece

Conclusão

Value classes são uma das features mais subestimadas do Kotlin. Elas oferecem type safety sem custo de runtime na maioria dos cenários — algo que poucas linguagens conseguem entregar. Se você quer escrever código Kotlin mais seguro, mais expressivo e mais performático, comece adotando value classes nos seus IDs e tipos primitivos de domínio. Para mais sobre modelagem e boas práticas, confira nosso guia de design patterns em Kotlin e o artigo sobre scope functions.

Se você vem de outras linguagens, vale notar que Go usa type definitions com custo zero semelhante, enquanto Rust tem o padrão newtype que inspira as value classes do Kotlin. Já em Python, o módulo typing oferece NewType para segurança estática, embora sem os ganhos de performance em runtime que o Kotlin proporciona.