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 init para 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 @JvmInline e 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.