O que é Value Class em Kotlin?
Uma value class (anteriormente chamada de inline class) em Kotlin é um tipo wrapper que encapsula um único valor sem adicionar overhead de alocacao em tempo de execução. O compilador substitui a value class pelo valor contido sempre que possível, eliminando a criação de objetos extras na heap.
Esse recurso resolve um problema clássico: você quer segurança de tipos para evitar confusao entre valores com o mesmo tipo primitivo (como misturar um ID de usuário com um ID de produto, ambos Int), mas não quer pagar o custo de performance de criar objetos wrapper.
Sintaxe básica
Uma value class e declarada com a anotacao @JvmInline é a palavra-chave value:
@JvmInline
value class UserId(val id: Int)
@JvmInline
value class Email(val valor: String)
@JvmInline
value class Reais(val valor: Double)
fun buscarUsuario(id: UserId): String {
return "Usuario #${id.id}"
}
fun main() {
val userId = UserId(42)
println(buscarUsuario(userId)) // Usuario #42
// buscarUsuario(42) // ERRO: Int nao e UserId
// buscarUsuario(Email("test@mail.com")) // ERRO: Email nao e UserId
}
A segurança de tipos e garantida em tempo de compilação, mas em tempo de execução o compilador usa o valor primitivo diretamente na maioria dos casos.
Como funciona a otimização
O compilador Kotlin trata value classes de forma especial. Considere este exemplo:
@JvmInline
value class Metros(val valor: Double)
fun calcularArea(largura: Metros, altura: Metros): Double {
return largura.valor * altura.valor
}
fun main() {
val largura = Metros(5.0)
val altura = Metros(3.0)
println(calcularArea(largura, altura)) // 15.0
}
No bytecode gerado, a função calcularArea recebe dois parametros Double diretamente, sem criar objetos Metros. A assinatura compilada se parece com:
// Bytecode equivalente (simplificado)
public static double calcularArea(double largura, double altura) {
return largura * altura;
}
Porem, há situacoes onde o boxing (empacotamento em objeto) acontece: quando a value class e usada como tipo nullable, em coleções genericas ou como tipo de interface.
Exemplos práticos
Dominio com tipos seguros
@JvmInline
value class CPF(val numero: String) {
init {
require(numero.length == 11) { "CPF deve ter 11 digitos" }
require(numero.all { it.isDigit() }) { "CPF deve conter apenas digitos" }
}
fun formatado(): String {
return "${numero.substring(0, 3)}.${numero.substring(3, 6)}.${numero.substring(6, 9)}-${numero.substring(9)}"
}
}
@JvmInline
value class CNPJ(val numero: String) {
init {
require(numero.length == 14) { "CNPJ deve ter 14 digitos" }
}
}
@JvmInline
value class Telefone(val numero: String) {
init {
require(numero.length in 10..11) { "Telefone invalido" }
}
}
data class Cliente(
val nome: String,
val cpf: CPF,
val telefone: Telefone
)
fun registrarCliente(nome: String, cpf: CPF, telefone: Telefone): Cliente {
return Cliente(nome, cpf, telefone)
}
fun main() {
val cpf = CPF("12345678901")
val telefone = Telefone("11999887766")
val cliente = registrarCliente("Ana Silva", cpf, telefone)
println(cliente)
println(cpf.formatado()) // 123.456.789-01
// registrarCliente("Ana", telefone, cpf) // ERRO de compilacao: tipos trocados
}
Value class com interface
Value classes podem implementar interfaces, mas isso causa boxing:
interface Exibivel {
fun exibir(): String
}
@JvmInline
value class Percentual(val valor: Double) : Exibivel {
override fun exibir(): String = "${valor}%"
}
fun mostrar(item: Exibivel) {
println(item.exibir())
}
fun main() {
val desconto = Percentual(15.0)
println(desconto.exibir()) // 15.0% - sem boxing aqui
mostrar(desconto) // boxing acontece aqui pois o parametro e Exibivel
}
Unidades de medida
@JvmInline
value class Quilometros(val valor: Double) {
fun paraMilhas(): Milhas = Milhas(valor * 0.621371)
fun paraMetros(): Double = valor * 1000.0
operator fun plus(outro: Quilometros) = Quilometros(valor + outro.valor)
operator fun times(fator: Double) = Quilometros(valor * fator)
}
@JvmInline
value class Milhas(val valor: Double) {
fun paraQuilometros(): Quilometros = Quilometros(valor * 1.60934)
}
@JvmInline
value class Celsius(val valor: Double) {
fun paraFahrenheit(): Fahrenheit = Fahrenheit(valor * 9.0 / 5.0 + 32.0)
}
@JvmInline
value class Fahrenheit(val valor: Double) {
fun paraCelsius(): Celsius = Celsius((valor - 32.0) * 5.0 / 9.0)
}
fun main() {
val distancia1 = Quilometros(100.0)
val distancia2 = Quilometros(50.0)
val total = distancia1 + distancia2
println("${total.valor} km") // 150.0 km
println("${total.paraMilhas().valor} milhas") // 93.2... milhas
val temp = Celsius(25.0)
println("${temp.valor}C = ${temp.paraFahrenheit().valor}F") // 25.0C = 77.0F
}
Identificadores tipados para APIs
@JvmInline
value class PedidoId(val valor: Long)
@JvmInline
value class ProdutoId(val valor: Long)
@JvmInline
value class ClienteId(val valor: Long)
data class ItemPedido(
val produtoId: ProdutoId,
val quantidade: Int
)
fun criarPedido(clienteId: ClienteId, itens: List<ItemPedido>): PedidoId {
println("Pedido criado para cliente ${clienteId.valor} com ${itens.size} itens")
return PedidoId(System.currentTimeMillis())
}
fun main() {
val clienteId = ClienteId(1001)
val itens = listOf(
ItemPedido(ProdutoId(501), 2),
ItemPedido(ProdutoId(502), 1)
)
val pedidoId = criarPedido(clienteId, itens)
println("Pedido: ${pedidoId.valor}")
// criarPedido(ProdutoId(501), itens) // ERRO: ProdutoId nao e ClienteId
}
Quando usar value class
- Segurança de tipos para primitivos quando você quer distinguir entre valores que possuem o mesmo tipo subjacente, como IDs, moedas, unidades de medida.
- Domain-Driven Design para criar tipos que representam conceitos do dominio de negócio.
- APIs publicas onde a clareza dos tipos de parametro previne erros de uso.
- Validação no construtor usando blocos
initpara garantir invariantes do tipo. - Substituicao de type aliases quando você precisa de segurança de tipos real, não apenas um apelido.
Casos de Uso no Mundo Real
Identificadores tipados em APIs REST: sistemas de backend usam value classes para criar tipos distintos para cada identificador (
UserId,OrderId,ProductId). Isso impede erros como passar um ID de produto onde se espera um ID de usuário, um tipo de bug que seria silencioso comLongpuro e só apareceria em producao.Modelagem de dominio financeiro: aplicações financeiras usam value classes para representar moedas (
Reais,Dolar,Euro) e valores monetarios com validacao embutida. O blocoinitgarante invariantes como valores não-negativos, e a tipagem impede operações entre moedas diferentes sem conversao explicita.Validacao de documentos e dados de entrada: formularios e APIs que recebem CPF, CNPJ, email ou telefone usam value classes com validacao no construtor para garantir que apenas dados validos circulem pelo sistema. Uma vez criado um
CPF("12345678901"), o restante do código pode confiar que o valor e válido.Unidades de medida em aplicações cientificas e de engenharia: sistemas que lidam com grandezas fisicas (metros, quilogramas, segundos) usam value classes com operadores customizados para prevenir erros de conversao de unidades. Isso evita incidentes classicos como confundir metros com pes em calculos de navegação.
Boas Praticas
- Use value classes para qualquer tipo primitivo que represente um conceito de dominio distinto. Se você tem dois parametros
Longque significam coisas diferentes, value classes previnem a troca acidental. - Adicione validacao no bloco
initpara garantir invariantes do tipo. Isso cria um ponto único de validacao e garante que instancias invalidas nunca existam no sistema. - Implemente operadores customizados (
plus,minus,times,compareTo) quando fizer sentido semantico para o tipo. Isso torna o código mais expressivo e seguro. - Esteja ciente de que boxing ocorre em contextos genericos (
List<MinhaValueClass>), nullable (MinhaValueClass?) e quando a value class implementa uma interface. Em caminhos criticos de performance, considere usar arrays primitivos quando possível. - Prefira value classes a type aliases quando seguranca de tipos for importante. O custo de digitacao adicional e compensado pela prevencao de bugs em tempo de compilação.
Perguntas Frequentes
P: Qual a diferenca entre value class e type alias?
R: Um type alias e apenas um apelido para um tipo existente e não oferece seguranca de tipos adicional. typealias UserId = Int permite usar UserId e Int de forma intercambiavel. Ja uma value class cria um tipo distinto: value class UserId(val id: Int) impede que um Int comum seja passado onde se espera um UserId, gerando erro de compilação.
P: Por que value classes só podem ter uma única propriedade no construtor? R: A otimização de inline depende de substituir o objeto wrapper pelo valor primitivo no bytecode. Com uma única propriedade, o compilador sabe exatamente qual valor usar como substituto. Com múltiplas propriedades, não seria possível representar o objeto como um único valor primitivo, e a otimização não funcionaria. Para múltiplas propriedades, use data classes.
P: Posso usar value classes com frameworks de serialização como Kotlinx Serialization ou Jackson?
R: Sim, mas requer configuração. Com Kotlinx Serialization, basta anotar a value class com @Serializable e ela sera serializada como o valor contido. Com Jackson, e necessário configurar o modulo jackson-module-kotlin e pode ser preciso adicionar anotacoes para controlar a serialização e desserializacao corretamente.
P: A anotacao @JvmInline e sempre necessária?
R: Na JVM (Kotlin/JVM), sim, e obrigatória. Ela indica ao compilador que deve gerar bytecode otimizado para a JVM. Em outros targets como Kotlin/JS e Kotlin/Native, a anotacao não e necessária pois essas plataformas tem suas proprias estrategias de otimização para value classes.
Erros comuns
Tentar usar mais de uma propriedade
// ERRADO: value class so aceita uma propriedade no construtor
@JvmInline
value class Coordenada(val x: Double, val y: Double) // erro de compilacao
Value classes só podem encapsular um único valor. Para múltiplos valores, use uma data class.
Ignorar o boxing em coleções genericas
@JvmInline
value class Idade(val valor: Int)
fun main() {
// Boxing acontece aqui: List<Idade> armazena objetos
val idades: List<Idade> = listOf(Idade(25), Idade(30), Idade(18))
// Para performance critica, prefira IntArray
val idadesArray = intArrayOf(25, 30, 18)
}
Esquecer a anotacao @JvmInline
// ERRADO na JVM: falta @JvmInline
value class Token(val valor: String) // erro de compilacao na JVM
// CORRETO
@JvmInline
value class Token(val valor: String)
Na JVM, a anotacao @JvmInline e obrigatória. Em Kotlin/Native e Kotlin/JS, ela não e necessária.
Heranca de classes
// ERRADO: value classes nao podem ser herdadas
@JvmInline
value class Base(val valor: Int)
// class Derivada(valor: Int) : Base(valor) // erro
// Value classes são implicitamente final
Termos relacionados
- Type alias: cria um nome alternativo para um tipo existente, mas sem segurança de tipos adicional.
- Data class: classe que gera automaticamente equals, hashCode, toString e copy, mas com alocacao de objeto.
- Inline function: função cujo corpo e copiado no ponto de chamada, conceito diferente de inline/value class.
- Wrapper pattern: padrão de design que encapsula um valor, que e exatamente o que value classes fazem sem overhead.
- Kotlin/JVM: plataforma onde a anotacao
@JvmInlinee obrigatória para value classes.
Conclusão
Value classes são a solução ideal quando você precisa de segurança de tipos para valores simples sem sacrificar performance. Elas combinam a clareza de tipos de dominio com a eficiencia de tipos primitivos. Use-as para IDs, unidades de medida, documentos e qualquer conceito que se beneficie de tipagem forte. Lembre-se de que o boxing acontece em contextos genericos e nullable, e que cada value class encapsula exatamente um valor.