Neste tutorial, vamos aprender tudo sobre herança em Kotlin. Herança é um dos pilares da programação orientada a objetos e permite que uma classe filha reutilize e estenda o comportamento de uma classe pai. Diferente de Java, onde todas as classes são abertas para herança por padrão, o Kotlin adota uma postura mais segura: classes são finais por padrão. Vamos explorar como usar open, override, super, classes abstratas, introdução a sealed classes e a classe Any.

A Classe Any: A Raiz de Tudo

Em Kotlin, todas as classes herdam implicitamente de Any. Essa classe é a raiz da hierarquia de tipos e fornece três métodos fundamentais: equals(), hashCode() e toString(). Qualquer classe que você crie já possui esses métodos herdados.

class MinhaClasse

fun main() {
    val obj = MinhaClasse()

    // Métodos herdados de Any
    println(obj.toString())    // MinhaClasse@<hash>
    println(obj.hashCode())    // Código hash numérico
    println(obj.equals(obj))   // true

    // 'is' verifica o tipo (como instanceof em Java)
    println(obj is Any)        // true — tudo é Any
}

Diferente de Java, onde Object é a raiz, Any em Kotlin não possui os métodos wait(), notify() e clone(). Essa simplificação reflete a filosofia do Kotlin de manter a API limpa e moderna.

Open Classes e Herança Básica

Por padrão, todas as classes em Kotlin são final — não podem ser herdadas. Para permitir que uma classe sirva de base para outras, você precisa marcá-la explicitamente com o modificador open. Da mesma forma, métodos e propriedades que podem ser sobrescritos também devem ser open.

open class Animal(val nome: String, val peso: Double) {

    open fun emitirSom(): String {
        return "$nome faz um som."
    }

    open fun descricao(): String {
        return "Animal: $nome, Peso: ${peso}kg"
    }

    // Função SEM open — não pode ser sobrescrita
    fun identificador(): String = "Animal-${nome.uppercase()}"
}

class Cachorro(nome: String, peso: Double, val raca: String) : Animal(nome, peso) {

    override fun emitirSom(): String {
        return "$nome late: Au au!"
    }

    override fun descricao(): String {
        return "Cachorro: $nome ($raca), Peso: ${peso}kg"
    }
}

class Gato(nome: String, peso: Double) : Animal(nome, peso) {

    override fun emitirSom(): String {
        return "$nome mia: Miau!"
    }
}

fun main() {
    val dog = Cachorro("Rex", 25.0, "Pastor Alemão")
    val cat = Gato("Mimi", 4.5)

    println(dog.emitirSom())    // Rex late: Au au!
    println(dog.descricao())    // Cachorro: Rex (Pastor Alemão), Peso: 25.0kg
    println(dog.identificador()) // Animal-REX

    println(cat.emitirSom())    // Mimi mia: Miau!
    println(cat.descricao())    // Animal: Mimi, Peso: 4.5kg
}

A decisão de tornar classes finais por padrão é uma escolha deliberada de design. O livro “Effective Java” de Joshua Bloch recomenda “projetar para herança ou proibi-la”, e o Kotlin segue exatamente esse princípio. Você precisa pensar conscientemente antes de abrir uma classe para extensão.

Usando super para Chamar a Classe Pai

A palavra-chave super permite acessar implementações da classe pai dentro de uma classe filha. Isso é útil quando você quer estender o comportamento existente em vez de substituí-lo completamente.

open class Veiculo(val marca: String, val modelo: String, val ano: Int) {

    open fun info(): String {
        return "$marca $modelo ($ano)"
    }

    open fun ligar(): String {
        return "Veículo ligado. Verificações básicas realizadas."
    }
}

class CarroEletrico(
    marca: String,
    modelo: String,
    ano: Int,
    val autonomiaKm: Int
) : Veiculo(marca, modelo, ano) {

    override fun info(): String {
        // Usa a implementação da classe pai e adiciona informação
        return "${super.info()} — Elétrico, Autonomia: ${autonomiaKm}km"
    }

    override fun ligar(): String {
        val basico = super.ligar()
        return "$basico\nBateria verificada. Motor elétrico pronto."
    }
}

fun main() {
    val tesla = CarroEletrico("Tesla", "Model 3", 2024, 450)
    println(tesla.info())
    // Tesla Model 3 (2024) — Elétrico, Autonomia: 450km

    println(tesla.ligar())
    // Veículo ligado. Verificações básicas realizadas.
    // Bateria verificada. Motor elétrico pronto.
}

O padrão de chamar super e depois adicionar comportamento específico é muito comum e é considerado boa prática, pois garante que a lógica da classe pai sempre será executada.

Classes Abstratas

Classes abstratas não podem ser instanciadas diretamente e podem conter tanto membros abstratos (sem implementação) quanto membros concretos (com implementação). Diferente de classes open, classes abstratas já são abertas para herança por natureza.

abstract class FormaPagamento(val titular: String) {

    // Método abstrato: subclasses DEVEM implementar
    abstract fun processar(valor: Double): Boolean
    abstract fun nomeMetodo(): String

    // Método concreto: subclasses herdam automaticamente
    fun recibo(valor: Double): String {
        return "Recibo: R$${"%.2f".format(valor)} via ${nomeMetodo()} - Titular: $titular"
    }
}

class CartaoCredito(titular: String, val bandeira: String) : FormaPagamento(titular) {

    override fun processar(valor: Double): Boolean {
        println("Processando R$${"%.2f".format(valor)} no cartão $bandeira de $titular...")
        return valor <= 5000.0 // Limite simplificado
    }

    override fun nomeMetodo() = "Cartão $bandeira"
}

class Pix(titular: String, val chave: String) : FormaPagamento(titular) {

    override fun processar(valor: Double): Boolean {
        println("Processando Pix de R$${"%.2f".format(valor)} para chave $chave...")
        return true // Pix sempre processa (simplificação)
    }

    override fun nomeMetodo() = "Pix ($chave)"
}

fun main() {
    val pagamentos: List<FormaPagamento> = listOf(
        CartaoCredito("Maria Silva", "Visa"),
        Pix("João Santos", "joao@email.com")
    )

    for (pagamento in pagamentos) {
        val sucesso = pagamento.processar(150.0)
        if (sucesso) {
            println(pagamento.recibo(150.0))
        }
        println()
    }
}

Classes abstratas são ideais quando você quer fornecer uma implementação parcial que subclasses devem completar. Elas definem um contrato enquanto oferecem código reutilizável — um equilíbrio entre interfaces (totalmente abstratas) e classes concretas.

Introdução a Sealed Classes

Sealed classes restringem a hierarquia de herança a um conjunto finito e conhecido de subclasses. Todas as subclasses diretas devem ser declaradas no mesmo arquivo. Isso permite que o compilador saiba exatamente quais tipos são possíveis, habilitando verificações exaustivas com when.

sealed class Resultado {
    data class Sucesso(val dados: String) : Resultado()
    data class Erro(val mensagem: String, val codigo: Int) : Resultado()
    data object Carregando : Resultado()
}

fun tratarResultado(resultado: Resultado): String {
    // O 'when' é exaustivo: o compilador garante que todos os casos são cobertos
    return when (resultado) {
        is Resultado.Sucesso -> "Dados: ${resultado.dados}"
        is Resultado.Erro -> "Erro ${resultado.codigo}: ${resultado.mensagem}"
        is Resultado.Carregando -> "Aguarde, carregando..."
        // Não precisa de 'else' — todos os tipos estão cobertos
    }
}

fun main() {
    val resultados = listOf(
        Resultado.Carregando,
        Resultado.Sucesso("Lista de usuários carregada"),
        Resultado.Erro("Não encontrado", 404)
    )

    for (r in resultados) {
        println(tratarResultado(r))
    }
}

Sealed classes são extremamente úteis para modelar estados de uma aplicação, respostas de API e qualquer cenário onde o conjunto de possibilidades é fechado e conhecido. Elas são amplamente usadas no desenvolvimento Android moderno com Jetpack Compose.

Sobrescrevendo Properties

Além de métodos, você também pode sobrescrever propriedades da classe pai. A propriedade na classe pai deve ser open, e a subclasse usa override.

open class Conta(open val limite: Double = 1000.0) {
    open val tipo: String = "Básica"

    fun info() = "Conta $tipo — Limite: R$${"%.2f".format(limite)}"
}

class ContaPremium : Conta() {
    override val limite: Double = 10000.0
    override val tipo: String = "Premium"
}

class ContaEmpresarial(override val limite: Double) : Conta() {
    override val tipo: String = "Empresarial"
}

fun main() {
    val basica = Conta()
    val premium = ContaPremium()
    val empresarial = ContaEmpresarial(50000.0)

    println(basica.info())        // Conta Básica — Limite: R$1000.00
    println(premium.info())       // Conta Premium — Limite: R$10000.00
    println(empresarial.info())   // Conta Empresarial — Limite: R$50000.00
}

Uma propriedade val na classe pai pode ser sobrescrita por uma var na subclasse (ampliando o acesso), mas o contrário não é permitido. Isso segue o princípio de que uma subclasse pode ser mais permissiva, mas não mais restritiva.

Erros Comuns

Esquecer de marcar a classe com open. Tentar herdar de uma classe sem o modificador open resulta em erro de compilação. Essa é a causa mais frequente de confusão para desenvolvedores vindos de Java.

Esquecer override no método sobrescrito. Diferente de Java onde @Override é opcional, em Kotlin o override é obrigatório. Omiti-lo causa erro de compilação.

Chamar métodos abstratos no construtor. Chamar um método open ou abstrato dentro do init block ou construtor da classe pai é perigoso, pois a subclasse ainda não foi completamente inicializada nesse ponto.

Confundir sealed class com enum. Sealed classes permitem que cada subclasse tenha propriedades e estados diferentes. Enums são constantes únicas. Use sealed quando os tipos filhos precisam de dados distintos.

Conclusão e Próximos Passos

Neste tutorial, você aprendeu os fundamentos de herança em Kotlin: desde a classe Any, passando por open classes, override, super, classes abstratas, sealed classes e sobrescrita de propriedades. Esses conceitos permitem criar hierarquias de tipos seguras e expressivas.

O próximo passo natural é estudar interfaces em Kotlin, que complementam a herança permitindo múltiplas implementações. Também recomendamos explorar delegation, uma alternativa poderosa à herança que favorece composição. Com domínio de herança e interfaces, você terá uma compreensão completa de OOP em Kotlin e estará pronto para projetar arquiteturas robustas em seus projetos.