O que e Boxing em Kotlin?

Boxing e o processo de encapsular um tipo primitivo (como Int, Long, Double) dentro de um objeto wrapper correspondente (como java.lang.Integer, java.lang.Long, java.lang.Double). O processo inverso, extrair o valor primitivo do objeto, e chamado de unboxing.

Em Kotlin, diferente do Java, voce nao declara explicitamente int vs Integer. Tudo e Int. Porem, o compilador decide por baixo dos panos se vai usar o tipo primitivo (int) ou o wrapper (Integer) dependendo do contexto. Entender quando o boxing acontece e fundamental para escrever codigo performatico.

Como o compilador decide?

O Kotlin compila tipos como Int para primitivos JVM (int) sempre que possivel. Porem, em certas situacoes, o boxing e obrigatorio:

  • Quando o tipo e nullable (Int?), o valor precisa ser boxed porque primitivos nao podem ser null.
  • Quando o tipo e usado como argumento generico (List<Int>), pois generics na JVM trabalham com objetos.
  • Quando o tipo e usado em contextos que exigem referencia de objeto.
val a: Int = 42       // Compilado como primitivo int
val b: Int? = 42      // Compilado como Integer (boxed)
val lista: List<Int> = listOf(1, 2, 3) // Cada Int e boxed como Integer

Impacto na performance

O boxing tem custo. Cada vez que um primitivo e boxed, um novo objeto e alocado no heap. Em loops intensivos ou colecoes grandes, isso pode gerar pressao significativa no garbage collector.

// Versao com boxing: cada iteracao cria um objeto Integer
fun somaBoxed(lista: List<Int>): Int {
    var soma = 0
    for (valor in lista) {
        soma += valor // unboxing acontece aqui
    }
    return soma
}

// Versao sem boxing: usa IntArray com primitivos
fun somaPrimitiva(array: IntArray): Int {
    var soma = 0
    for (valor in array) {
        soma += valor // nenhum boxing
    }
    return soma
}

A segunda versao e significativamente mais rapida para arrays grandes porque nao envolve alocacao de objetos nem garbage collection adicional.

Arrays especializados

Para evitar boxing em arrays, Kotlin oferece tipos especializados:

val intArray: IntArray = intArrayOf(1, 2, 3)          // int[] na JVM
val longArray: LongArray = longArrayOf(1L, 2L, 3L)    // long[] na JVM
val doubleArray: DoubleArray = doubleArrayOf(1.0, 2.0) // double[] na JVM

// Comparando com Array<Int> que sofre boxing
val boxedArray: Array<Int> = arrayOf(1, 2, 3) // Integer[] na JVM

Sempre prefira IntArray, LongArray, DoubleArray e similares quando performance for importante.

Identidade vs igualdade com boxing

Um detalhe sutil e que o boxing pode afetar a identidade de objetos:

val a: Int = 127
val boxedA: Int? = a
val outroBoxedA: Int? = a
println(boxedA === outroBoxedA) // true (cache de -128 a 127)

val b: Int = 128
val boxedB: Int? = b
val outroBoxedB: Int? = b
println(boxedB === outroBoxedB) // false (fora do cache)
println(boxedB == outroBoxedB)  // true (igualdade estrutural)

A JVM faz cache de Integer para valores entre -128 e 127. Fora dessa faixa, cada boxing cria um novo objeto, entao a comparacao de identidade (===) retorna false. Sempre use == para comparar valores numericos.

Boxing em funcoes genericas

Funcoes genericas sempre causam boxing porque a JVM nao suporta generics com primitivos:

fun <T> imprimir(valor: T) {
    println(valor)
}

fun main() {
    imprimir(42) // 42 e boxed para Integer antes de ser passado
}

Para evitar isso em casos especificos, voce pode usar funcoes inline com reified:

inline fun <reified T> verificarTipo(valor: T): String {
    return when (T::class) {
        Int::class -> "Inteiro"
        String::class -> "String"
        else -> "Outro"
    }
}

Porem, isso nao elimina completamente o boxing em todos os cenarios. Para performance critica, considere sobrecarregar a funcao com versoes especificas para cada tipo primitivo.

Quando usar tipos que causam boxing

O boxing nao e algo que voce precisa evitar obsessivamente. Na maioria dos casos, o impacto e negligenciavel. Preocupe-se com boxing apenas quando:

  • Voce esta processando milhoes de elementos em loops apertados.
  • Voce esta trabalhando com colecoes muito grandes onde a alocacao de memoria importa.
  • Profiling mostrou que garbage collection esta sendo um gargalo no seu sistema.
  • Voce esta desenvolvendo bibliotecas de alta performance ou algoritmos numericos.

Para codigo de aplicacao comum, como chamadas de API, manipulacao de UI ou logica de negocios, o boxing e perfeitamente aceitavel e nao merece otimizacao prematura.

Erros comuns

  1. Comparar tipos nullable com ===: como vimos, boxing pode criar objetos diferentes para o mesmo valor. Sempre use == para comparacoes de valor.

  2. Ignorar arrays especializados: usar Array<Int> em vez de IntArray quando performance importa. A diferenca pode ser significativa em colecoes grandes.

  3. Otimizar prematuramente: evitar List<Int> em favor de IntArray em todo lugar, mesmo quando a lista e pequena e a legibilidade do codigo e mais importante.

  4. Esquecer que nullable causa boxing: declarar var contador: Int? = 0 quando o valor nunca sera null. Use Int sem interrogacao sempre que possivel.

  5. Nao considerar o boxing em benchmarks: ao comparar performance de diferentes abordagens, o boxing pode distorcer resultados se nao for levado em conta.

Colecoes sem boxing

Para cenarios de alta performance, existem bibliotecas que oferecem colecoes especializadas sem boxing, como a biblioteca eclipse-collections ou implementacoes customizadas com IntArray como backing store:

class ListaDeInteiros {
    private var dados = IntArray(16)
    private var tamanho = 0

    fun adicionar(valor: Int) {
        if (tamanho == dados.size) {
            dados = dados.copyOf(dados.size * 2)
        }
        dados[tamanho++] = valor
    }

    fun obter(indice: Int): Int = dados[indice]

    fun tamanho(): Int = tamanho
}

Termos relacionados

  • Value Class: permite criar tipos que encapsulam um primitivo sem overhead de boxing adicional em tempo de execucao.
  • Inline: funcoes inline podem ajudar a reduzir boxing em certos contextos com lambdas.
  • Reified: permite acessar o tipo generico em tempo de execucao em funcoes inline, evitando algumas formas de boxing.
  • Nullable: tipos nullable (Int?) sempre causam boxing porque primitivos nao podem ser null na JVM.
  • IntArray/LongArray/DoubleArray: arrays especializados que evitam boxing, armazenando primitivos diretamente.

Compreender boxing e unboxing e essencial para escrever Kotlin que seja ao mesmo tempo idiomatico e eficiente. Na maioria dos casos, confie no compilador. Nos casos onde performance e critica, use as ferramentas que o Kotlin oferece para manter tudo no mundo dos primitivos.