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.