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
@JvmInlineobrigató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:
- Apenas uma propriedade no construtor primário (por enquanto)
- Não podem ter
lateinitou propriedades delegadas - Não podem participar de hierarquias de classe — não podem ser
open,abstractousealed - Boxing em contextos genéricos e nullable
- 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
| Aspecto | Value Class | Data Class | Type Alias |
|---|---|---|---|
| Type safety | ✅ Tipo distinto | ✅ Tipo distinto | ❌ Mesmo tipo |
| Alocação heap | ❌ Geralmente não | ✅ Sempre | N/A |
| Múltiplas props | ❌ Apenas uma | ✅ Sim | N/A |
| equals/hashCode | ✅ Automático | ✅ Automático | N/A |
| copy() | ❌ Não | ✅ Sim | N/A |
Boas Práticas
- Use value classes para IDs e identificadores — é o caso de uso com melhor relação custo-benefício
- Adicione validação no
init— falhe rápido em vez de propagar dados inválidos - Prefira value classes a type aliases quando precisar de type safety real
- Evite em contextos genéricos se performance for crítica (listas, maps, nullable)
- Combine com sealed classes para modelagem de domínio expressiva
- 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.