O que e Value Class em Kotlin?
Uma value class (anteriormente chamada de inline class) em Kotlin e um tipo wrapper que encapsula um unico valor sem adicionar overhead de alocacao em tempo de execucao. O compilador substitui a value class pelo valor contido sempre que possivel, eliminando a criacao de objetos extras na heap.
Esse recurso resolve um problema classico: voce quer seguranca de tipos para evitar confusao entre valores com o mesmo tipo primitivo (como misturar um ID de usuario com um ID de produto, ambos Int), mas nao quer pagar o custo de performance de criar objetos wrapper.
Sintaxe basica
Uma value class e declarada com a anotacao @JvmInline e 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 seguranca de tipos e garantida em tempo de compilacao, mas em tempo de execucao o compilador usa o valor primitivo diretamente na maioria dos casos.
Como funciona a otimizacao
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 funcao 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, ha situacoes onde o boxing (empacotamento em objeto) acontece: quando a value class e usada como tipo nullable, em colecoes genericas ou como tipo de interface.
Exemplos praticos
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
- Seguranca de tipos para primitivos quando voce 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 negocio.
- APIs publicas onde a clareza dos tipos de parametro previne erros de uso.
- Validacao no construtor usando blocos
initpara garantir invariantes do tipo. - Substituicao de type aliases quando voce precisa de seguranca de tipos real, nao apenas um apelido.
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 so podem encapsular um unico valor. Para multiplos valores, use uma data class.
Ignorar o boxing em colecoes 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 obrigatoria. Em Kotlin/Native e Kotlin/JS, ela nao e necessaria.
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 sao implicitamente final
Termos relacionados
- Type alias: cria um nome alternativo para um tipo existente, mas sem seguranca de tipos adicional.
- Data class: classe que gera automaticamente equals, hashCode, toString e copy, mas com alocacao de objeto.
- Inline function: funcao cujo corpo e copiado no ponto de chamada, conceito diferente de inline/value class.
- Wrapper pattern: padrao de design que encapsula um valor, que e exatamente o que value classes fazem sem overhead.
- Kotlin/JVM: plataforma onde a anotacao
@JvmInlinee obrigatoria para value classes.
Conclusao
Value classes sao a solucao ideal quando voce precisa de seguranca 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.