Por que o Kotlin não tem static, mas tem companion object? Entendendo desempenho e lógica
Se você está migrando do Java para o Kotlin, uma das primeiras perguntas que surgirá no StackOverflow (e que realmente acumulou milhares de visualizações) é: «Por que usamos companion object em vez do Java static? E isso não deixa a aplicação lenta?»
Vamos ser honestos: quando vi pela primeira vez a construção companion object, pensei que a equipe do Kotlin só queria complicar a vida. Por que escrever mais código se poderiam simplesmente copiar o static do Java? Mas, investigando mais a fundo, entendi: não é um capricho, mas uma decisão arquitetural bem pensada que torna o código mais limpo, seguro e… sim, às vezes até mais rápido.
Neste artigo, não vamos apenas traduzir a resposta do SO. Vamos analisar como o companion object funciona internamente, comparar o desempenho com o Java static e mostrar quando usá-lo, e quando é melhor escolher outro caminho.
O que é companion object em termos simples?
Em Java, campos e métodos estáticos pertencem à classe, não à instância. O Kotlin decidiu abandonar a palavra-chave static porque ela viola os princípios de POO (o estático não pode ser sobrescrito, não faz parte de um objeto). Em vez disso, o Kotlin oferece objetos companheiros.
Imagine que você tem uma classe User. Você quer armazenar uma constante MAX_AGE e um método de fábrica createGuest(). Em Java, você escreveria:
public class User { public static final int MAX_AGE = 120; public static User createGuest() { return new User("guest", "1234"); }}Em Kotlin, fica assim:
class User(private val name: String, private val password: String) { companion object { const val MAX_AGE = 120 fun createGuest() = User("guest", "1234") }}Observe: companion object não é apenas um «bloco static». É um objeto completo que vive dentro da sua classe. Ele pode ter interfaces, extensões e até um nome próprio (se você definir).
Como o companion object funciona internamente? (Pelos olhos da JVM)
Para entender o desempenho, vamos dar uma olhada no bytecode. Quando você escreve um companion object, o compilador Kotlin faz o seguinte:
- Cria uma classe estática interna com o nome
Companion(ou o nome personalizado que você definir). - Coloca todos os campos e métodos do companion object dentro dessa classe.
- Se você usar a anotação
@JvmStatic, o compilador gera adicionalmente métodos estáticos wrapper na própria classe.
Exemplo com a anotação @JvmStatic:
class User { companion object { @JvmStatic fun createGuest() = User() }}Agora, do Java, você pode chamar User.createGuest() como um método estático normal. Sem essa anotação, você teria que escrever User.Companion.createGuest().
Conclusão sobre desempenho: se você trabalha apenas com Kotlin, não há custo adicional. Se chamar do Java, há um nível extra de indireção (através do objeto Companion). Mas o compilador JIT da JVM otimiza muito bem essas chamadas, então a diferença de velocidade tende a zero.
Desempenho: companion object vs Java static
Vamos desfazer o mito principal: companion object NÃO deixa lento. Realizei alguns micro-benchmarks (no Kotlin 1.9, JVM 17), e aqui estão os resultados: