O que é Operator Overloading em Kotlin?

Operator Overloading (sobrecarga de operadores) permite redefinir o comportamento de operadores como +, -, *, [], == e outros para suas próprias classes. Você escreve funções especiais marcadas com operator e pronto — seus objetos passam a funcionar com a sintaxe natural de operadores.

Exemplo básico: somando vetores

data class Vetor(val x: Double, val y: Double) {
    operator fun plus(outro: Vetor) = Vetor(x + outro.x, y + outro.y)
    operator fun minus(outro: Vetor) = Vetor(x - outro.x, y - outro.y)
    operator fun times(escalar: Double) = Vetor(x * escalar, y * escalar)
}

fun main() {
    val a = Vetor(1.0, 2.0)
    val b = Vetor(3.0, 4.0)

    println(a + b)      // Vetor(x=4.0, y=6.0)
    println(a - b)      // Vetor(x=-2.0, y=-2.0)
    println(a * 3.0)    // Vetor(x=3.0, y=6.0)
}

Principais operadores

OperadorFunçãoExemplo
+plusa + b
-minusa - b
*timesa * b
/diva / b
[]get / seta[i]
==equalsa == b
> <compareToa > b
incontainsx in a

Operadores de acesso: get e set

class Matriz(private val dados: Array<IntArray>) {
    operator fun get(linha: Int, coluna: Int): Int = dados[linha][coluna]
    operator fun set(linha: Int, coluna: Int, valor: Int) {
        dados[linha][coluna] = valor
    }
}

fun main() {
    val m = Matriz(arrayOf(intArrayOf(1, 2), intArrayOf(3, 4)))
    println(m[0, 1]) // 2
    m[1, 0] = 99
    println(m[1, 0]) // 99
}

Operador invoke

O operador invoke permite chamar um objeto como se fosse uma função:

class Validador(val regex: Regex) {
    operator fun invoke(valor: String): Boolean = regex.matches(valor)
}

fun main() {
    val validarEmail = Validador(Regex("^[\\w.-]+@[\\w.-]+\\.[a-z]{2,}$"))
    println(validarEmail("karina@kotlin.dev.br")) // true
    println(validarEmail("invalido"))              // false
}

Operadores de atribuição composta

Além dos operadores básicos, você pode sobrecarregar os operadores de atribuição composta como +=, -= e *=. O Kotlin resolve automaticamente a += b como a = a.plus(b) se a variável for var, ou você pode definir plusAssign explicitamente para tipos mutáveis:

class Carrinho {
    private val itens = mutableListOf<String>()

    operator fun plusAssign(item: String) {
        itens.add(item)
    }

    operator fun minusAssign(item: String) {
        itens.remove(item)
    }

    operator fun contains(item: String): Boolean = item in itens

    override fun toString() = "Carrinho(itens=$itens)"
}

fun main() {
    val carrinho = Carrinho()
    carrinho += "Teclado"
    carrinho += "Mouse"
    carrinho += "Monitor"
    carrinho -= "Mouse"

    println(carrinho)                // Carrinho(itens=[Teclado, Monitor])
    println("Teclado" in carrinho)   // true
    println("Mouse" in carrinho)     // false
}

Operadores como extensões

Você não precisa ser o dono da classe para definir operadores. Funções de extensão com operator funcionam perfeitamente:

data class Dinheiro(val centavos: Long) {
    override fun toString(): String {
        val reais = centavos / 100
        val cents = centavos % 100
        return "R$ $reais,${cents.toString().padStart(2, '0')}"
    }
}

operator fun Dinheiro.plus(outro: Dinheiro) = Dinheiro(centavos + outro.centavos)
operator fun Dinheiro.times(quantidade: Int) = Dinheiro(centavos * quantidade)
operator fun Dinheiro.compareTo(outro: Dinheiro) = centavos.compareTo(outro.centavos)

fun main() {
    val preco = Dinheiro(2990)       // R$ 29,90
    val frete = Dinheiro(1500)       // R$ 15,00
    val total = preco + frete
    val triplo = preco * 3

    println(total)   // R$ 44,90
    println(triplo)  // R$ 89,70
    println(preco > frete) // true
}

Casos de Uso no Mundo Real

  • Cálculos financeiros: criar tipos como Dinheiro ou Moeda com operadores aritméticos, evitando erros de arredondamento com Double e garantindo segurança de tipo.
  • Bibliotecas matemáticas e científicas: definir operações sobre vetores, matrizes, números complexos e outras estruturas algébricas, permitindo que fórmulas no código pareçam com fórmulas matemáticas.
  • DSLs (Domain-Specific Languages): usar invoke, get e contains para criar APIs fluentes e naturais, como builders de consultas SQL ou configurações.
  • Coleções customizadas: sobrecarregar get, set, contains, plusAssign e iterator para criar estruturas de dados com interface familiar.

Boas Práticas

  • O operador deve ter um significado intuitivo para quem lê o código. Somar dois vetores faz sentido; “somar” dois usuários, nem tanto.
  • Mantenha a semântica esperada: plus deve ser comutativo quando possível, compareTo deve ser transitivo, equals deve ser reflexivo, simétrico e transitivo.
  • Prefira operadores como extensões quando a classe não é sua. Isso mantém o código desacoplado e permite adicionar operadores a classes de bibliotecas de terceiros.
  • Documente operadores não óbvios. Se o significado de * na sua classe não for imediatamente claro, adicione um comentário explicando.
  • Evite sobrecarregar operadores em excesso. Se a classe tem dez operadores, talvez ela esteja fazendo coisas demais.

Erros Comuns

  • Usar operator overloading sem semântica clara: definir + para concatenar objetos que não fazem sentido juntos confunde outros desenvolvedores e torna o código difícil de manter.
  • Esquecer a palavra-chave operator: sem ela, a função é apenas uma função normal chamada plus ou times, e o operador + ou * não vai funcionar.
  • Confundir plus com plusAssign: se a classe é imutável, defina apenas plus e use var. Se é mutável, defina plusAssign. Definir ambos na mesma classe gera ambiguidade com var.
  • Não respeitar contratos de equals e hashCode: ao sobrecarregar equals, sempre sobrescreva hashCode também. Caso contrário, a classe vai se comportar de forma inesperada em coleções como HashSet e HashMap.
  • Ignorar efeitos colaterais em operadores: operadores devem ser puros quando possível. Um plus que faz chamada de rede ou modifica estado global é um design perigoso.

Perguntas Frequentes

Posso criar operadores novos que não existem em Kotlin? Não. Kotlin tem um conjunto fixo de operadores que podem ser sobrecarregados. Você não pode inventar símbolos novos como <> ou **. A lista completa está na documentação oficial.

Operator overloading afeta a performance? Na maioria dos casos, não há impacto significativo. O compilador transforma operadores em chamadas de função normais. Com data class e inline class, o overhead é mínimo ou zero.

Qual a diferença entre == e === ao sobrecarregar? O operador == chama a função equals e pode ser sobrecarregado. Já o === verifica identidade referencial (se dois objetos são a mesma instância na memória) e não pode ser sobrecarregado.

Posso sobrecarregar operadores em interfaces? Sim. Você pode declarar funções operator em interfaces, e as classes que implementarem a interface terão o operador disponível.

Termos Relacionados

  • Extension Function — funções de extensão que podem definir operadores para classes existentes
  • Data Class — classes que já implementam equals, hashCode e outros operadores automaticamente
  • Infix Function — outra forma de criar sintaxe expressiva em Kotlin
  • Inline Class — classes wrapper com zero overhead que combinam bem com operadores

Quando bem aplicado, operator overloading deixa o código expressivo e natural.