O que é Companion Object em Kotlin?

O companion object é um objeto especial declarado dentro de uma classe que funciona como o equivalente aos membros estáticos do Java. Com ele, você pode acessar propriedades e funções diretamente pelo nome da classe, sem precisar criar uma instância.

Kotlin não tem a palavra-chave static. Em vez disso, usa companion objects – uma abordagem mais flexível e orientada a objetos.

Pense no companion object como a “recepção” de um prédio comercial. Você não precisa entrar em nenhuma sala específica (instância) para obter informações gerais – basta perguntar na recepção. Da mesma forma, o companion object guarda informações e funcionalidades que pertencem à classe como um todo, não a uma instância individual.

Sintaxe básica

class Configuração {
    companion object {
        const val VERSAO = "2.1.0"
        val AMBIENTE = "producao"

        fun info(): String {
            return "Versão $VERSAO - Ambiente: $AMBIENTE"
        }
    }
}

fun main() {
    println(Configuração.VERSAO)    // 2.1.0
    println(Configuração.info())    // Versão 2.1.0 - Ambiente: producao
}

Repare que você acessa versão e info() direto pela classe, sem instanciar nada.

Factory methods

Um dos usos mais comuns do companion object é para criar factory methods – funções que constroem objetos de formas alternativas:

data class Usuario(val nome: String, val email: String) {
    companion object {
        fun criarAdmin(nome: String): Usuario {
            return Usuario(nome, "$nome@admin.com.br")
        }

        fun criarConvidado(): Usuario {
            return Usuario("Convidado", "convidado@temp.com.br")
        }
    }
}

fun main() {
    val admin = Usuario.criarAdmin("karina")
    val guest = Usuario.criarConvidado()

    println(admin)  // Usuario(nome=karina, email=karina@admin.com.br)
    println(guest)  // Usuario(nome=Convidado, email=convidado@temp.com.br)
}

Companion object com nome e interface

O companion object pode ter um nome e até implementar interfaces:

interface Serializavel {
    fun fromJson(json: String): Any
}

class Produto(val nome: String) {
    companion object Fabrica : Serializavel {
        override fun fromJson(json: String): Produto {
            return Produto(json) // simplificado
        }
    }
}

Quando usar?

Use companion object para constantes, factory methods, e qualquer funcionalidade que pertença à classe como um todo e não a uma instância específica. É o jeito Kotlin de lidar com o que seria static em Java.

Companion object com extension functions

Você pode adicionar extension functions ao companion object de uma classe, o que permite estender funcionalidades sem modificar o código original:

class Validador {
    companion object
}

fun Validador.Companion.cpf(valor: String): Boolean {
    return valor.replace("[^0-9]".toRegex(), "").length == 11
}

fun Validador.Companion.email(valor: String): Boolean {
    return valor.contains("@") && valor.contains(".")
}

fun main() {
    println(Validador.cpf("123.456.789-00")) // true
    println(Validador.email("teste@email.com")) // true
    println(Validador.email("invalido"))         // false
}

Companion object como singleton de configuração

Um padrão muito utilizado é usar companion object para gerenciar configurações ou registros de uma classe:

class Logger private constructor(val nome: String) {
    companion object {
        private val instancias = mutableMapOf<String, Logger>()

        fun obter(nome: String): Logger {
            return instancias.getOrPut(nome) {
                println("Criando logger para: $nome")
                Logger(nome)
            }
        }
    }

    fun info(mensagem: String) {
        println("[$nome] INFO: $mensagem")
    }
}

fun main() {
    val logA = Logger.obter("App")
    val logB = Logger.obter("App")    // reutiliza a instância existente
    val logC = Logger.obter("Database")

    logA.info("Aplicação iniciada")
    logC.info("Conexão estabelecida")

    println(logA === logB) // true -- mesma instância
}

Casos de Uso no Mundo Real

  • Constantes de configuração: agrupar URLs de API, chaves de preferências, códigos de erro e outras constantes que pertencem ao contexto da classe.
  • Factory methods com válidação: criar instâncias com regras de negócio, como Usuario.fromCsv(linha) ou Pedido.criarRascunho().
  • Interoperabilidade com Java: usar a anotação @JvmStatic em métodos do companion object para que o código Java possa chamar como métodos estáticos reais.
  • Serialização e desserialização: implementar funções fromJson() e fromMap() no companion object para converter dados externos em objetos Kotlin.
  • Registros e caches: manter um mapa de instâncias para evitar criação duplicada, como no padrão mostrado acima.

Boas Práticas

  • Use const val para constantes primitivas: dentro do companion object, declare constantes de tipos primitivos e String com const val para garantir que sejam inlined pelo compilador.
  • Não coloque lógica de negócio pesada no companion object. Ele deve conter funcionalidades utilitárias relacionadas à classe, não regras complexas.
  • Dê um nome ao companion object quando ele implementa uma interface ou quando você quer deixar o propósito claro: companion object Factory.
  • Use @JvmStatic em projetos mistos Kotlin/Java para facilitar a interoperabilidade.
  • Prefira object de nível superior quando a funcionalidade não está diretamente ligada a uma classe específica.

Erros Comuns

  • Confundir companion object com instância da classe: o companion object é compartilhado entre todas as instâncias. Alterar uma propriedade var dentro dele afeta todas as partes do código que a acessam.
  • Usar companion object para estado mutável global: isso cria acoplamento e dificulta testes. Se precisa de estado compartilhado, considere injeção de dependência.
  • Esquecer que cada classe só pode ter um companion object: se você precisa de múltiplos agrupamentos, use objects regulares nomeados dentro da classe.
  • Não usar const em constantes String: declarar val em vez de const val para strings constantes impede a otimização do compilador e gera um campo com getter desnecessário.

Perguntas Frequentes

Qual a diferença entre companion object e object declaration? O companion object vive dentro de uma classe e permite acessar seus membros pelo nome da classe (como MinhaClasse.função()). Um object declaration é um singleton independente, acessado pelo próprio nome.

Posso ter mais de um companion object por classe? Não. Cada classe pode ter no máximo um companion object. Se precisar de mais agrupamentos, use object nomeados regulares dentro da classe.

O companion object é inicializado quando? Ele é inicializado na primeira vez que a classe é referenciada, de forma similar ao carregamento de classes no Java. Isso segue o padrão de inicialização lazy.

Como acessar o companion object de Kotlin a partir do Java? Use MinhaClasse.Companion.método(). Se quiser acessar como método estático, adicione @JvmStatic ao método dentro do companion object.