Pular para o conteúdo

Trilha de Kotlin · parte 10 de 59

Herança e polimorfismo

Aprenda a reaproveitar e especializar classes com open, override, super e abstract, e a tratar objetos de tipos diferentes da mesma forma em Kotlin.

iniciantePublicada em · 11 min de leitura

Tamanho da fonte:
Largura da coluna:
Tom:
Espaço entre linhas:
Fonte:

Herança e

Imagine o formulário de cadastro de funcionário. Todo mundo preenche nome e salário, e o formulário do gerente não começa do zero: usa esse padrão como base, e uma mudança no padrão chega até ele. Herança é esse atalho. Uma classe nova parte de outra que já existe, recebe o que ela tem e acrescenta ou troca o que for diferente.

A frase que sustenta a herança é “um gerente é um funcionário”. Quando ela vale no domínio, a herança poupa código. Quando não vale, a classe herda comportamento que não lhe pertence, e boa parte dos erros desta aula nasce daí.

Esta aula supõe que você já declara classes e propriedades (Classes, objetos e propriedades) e conhece a visibilidade (Encapsulamento, visibilidade, object e companion). O domínio é uma pequena empresa com desenvolvedores, gerentes e estagiários, cada um com um jeito próprio de calcular o bônus.

o mapa da aula

Começamos pelo open, que o Kotlin exige para liberar a herança, e pelo override. Depois vêm super, o construtor do pai, abstract e o polimorfismo. Por fim, is, as, protected, a composição e os erros comuns. Interfaces ficam para a aula Interfaces, delegação e classes open.

Os exemplos rodaram com Kotlin 2.4.21, Gradle 9.7.0 e JDK 25. As posições de erro são do arquivo mostrado, sem package.

1. reaproveitar e especializar

A relação que justifica a herança é “é um”. Um estagiário é um funcionário, e recebe o nome, o salário e o método do total. Ele escreve só o próprio bônus, porque cada cargo calcula o seu. Sem a relação “é um”, o reaproveitamento vem pela composição, que a seção 10 discute.

2. herança com class

Para uma classe aceitar filhas, ela precisa ser marcada com open. O motivo dessa exigência está na seção 2 da aula Interfaces, delegação e classes open. A filha declara a herança depois de dois-pontos, chamando o construtor do pai:

open class Funcionario(val nome: String, val salarioBase: Double) {
    fun descricao(): String = "$nome, salário base de $salarioBase"
}

class Estagiario(nome: String, salarioBase: Double) : Funcionario(nome, salarioBase)

fun main() {
    val carla = Estagiario("Carla", 2000.0)
    println(carla.descricao())
    println(carla.nome)
}
Carla, salário base de 2000.0
Carla

A Estagiario não tem corpo e mesmo assim responde descricao() e expõe nome. Os valores de : Funcionario(nome, salarioBase) seguem para o construtor do pai, que guarda as propriedades. Por isso o construtor da filha não tem val. Quando o framework cria subclasses por conta própria, como o Spring faz com classes anotadas, o caso está na aula Interfaces, delegação e classes open.

Esquecer o open é erro típico de quem vem do Java, onde a herança é liberada por padrão. O compilador responde:

class Funcionario(val nome: String)

class Estagiario(nome: String) : Funcionario(nome)

fun main() {
    println(Estagiario("Carla").nome)
}
.../src/main/kotlin/Main.kt:3:34 This type is final, so it cannot be extended.

A posição aponta para o nome Funcionario, na linha 3.

3. de funções e propriedades

Uma filha que troca um método declara isso com override. O modificador é obrigatório: sem ele, o compilador não sabe se você quis substituir o método do pai ou criar outro com o mesmo nome por engano.

open class Funcionario(val nome: String, val salarioBase: Double) {
    open val cargo: String = "funcionário"

    open fun descricao(): String = "$nome ($cargo)"
}

class Gerente(nome: String, salarioBase: Double, val tamanhoEquipe: Int) : Funcionario(nome, salarioBase) {
    override val cargo = "gerente"

    override fun descricao(): String = super.descricao() + ", equipe de $tamanhoEquipe"
}

fun main() {
    val bruno = Gerente("Bruno", 12000.0, 4)
    println(bruno.descricao())
    println(bruno.cargo)
}
Bruno (gerente), equipe de 4
gerente

O descricao() do pai é open, e a Gerente o reescreve. A propriedade cargo também é open e recebe override val, com tipo compatível. Esquecer o override gera este erro:

open class Funcionario(val nome: String) {
    open fun bonus(): Double = 0.0
}

class Gerente(nome: String) : Funcionario(nome) {
    fun bonus(): Double = 500.0
}

fun main() {
    println(Gerente("Bruno").bonus())
}
.../src/main/kotlin/Main.kt:6:9 'bonus' hides member of supertype 'Funcionario' and needs an 'override' modifier.

4. : chamar a versão do pai

Às vezes a filha usa o que o pai já faz e acrescenta um detalhe. Na Gerente, super.descricao() devolve “Bruno (gerente)”, e a filha só acrescenta ”, equipe de 4”. Se essa montagem fosse repetida na filha, uma mudança no pai não chegaria até ela. O super aponta para a superclasse imediata.

5. construtor da superclasse

O construtor do pai roda antes do da filha. A Funcionario lê cargo no bloco init, que a aula Classes, objetos e propriedades apresenta, e a Gerente substitui cargo:

open class Funcionario(val nome: String) {
    open val cargo: String = "funcionário"

    init {
        println("a base vê o cargo: $cargo")
    }
}

class Gerente(nome: String) : Funcionario(nome) {
    override val cargo = "gerente"
}

fun main() {
    val bruno = Gerente("Bruno")
    println("depois da criação: ${bruno.cargo}")
}
a base vê o cargo: null
depois da criação: gerente

A base lê cargo antes de a propriedade da filha ter valor, e por isso recebe null. Depois da criação, o valor já existe. O compilador não emitiu aviso, então a saída é a única pista. A regra prática é que o construtor do pai não chame nada que a filha possa sobrescrever.

6. : quando o pai não faz sentido sozinho

Um funcionário sem cargo não existe na prática. O pai só organiza o que os cargos têm em comum, e para isso existe abstract: a classe pode ser herdada, mas não criada diretamente. Aqui, cargo e bonus() são abstract, e cada filha os fornece. Este é o domínio das seções seguintes:

// protected: a seção 9 explica o que isso permite.
abstract class Funcionario(val nome: String, protected val salarioBase: Double) {
    abstract val cargo: String
    abstract fun bonus(): Double

    fun total(): Double = salarioBase + bonus()

    open fun descricao(): String = "$nome ($cargo)"
}

class Desenvolvedor(nome: String, salarioBase: Double) : Funcionario(nome, salarioBase) {
    override val cargo = "desenvolvedor"

    override fun bonus(): Double = salarioBase * 0.10
}

class Gerente(nome: String, salarioBase: Double, val tamanhoEquipe: Int) : Funcionario(nome, salarioBase) {
    override val cargo = "gerente"

    override fun bonus(): Double = 500.0 * tamanhoEquipe

    override fun descricao(): String = super.descricao() + ", equipe de $tamanhoEquipe"
}

class Estagiario(nome: String, salarioBase: Double) : Funcionario(nome, salarioBase) {
    override val cargo = "estagiário"

    override fun bonus(): Double = 0.0
}

Compilado sozinho, sem main, o trecho passa sem aviso. O total é igual para todos; por isso ele não é abstract nem open. Uma filha que deixa de implementar um desses membros também não compila: testei uma Estagiario sem bonus(), e o compilador recusou a classe. Criar um funcionário direto falha:

abstract class Funcionario(val nome: String) {
    abstract fun bonus(): Double
}

fun main() {
    val ana = Funcionario("Ana")
    println(ana.nome)
}
.../src/main/kotlin/Main.kt:6:15 Cannot create an instance of an abstract class.
Funcionario abstract é pai de Desenvolvedor, Gerente e Estagiario, e criar um Funcionario direto aparece bloqueado pelo compilador.
figura 1 · Hierarquia de Funcionario: um pai abstract e três filhas

A figura mostra o recorte: a tentativa bloqueada em cima, o pai no meio e as três filhas embaixo. Não há seta, e o sentido vem do rótulo “pai de”. O corpo dos métodos fica de fora.

7. polimorfismo: a mesma chamada, comportamentos diferentes

Uma List<Funcionario> guarda desenvolvedores, gerentes e estagiários, porque todos são funcionários. Com as classes da seção 6 no mesmo arquivo, o laço chama total() sem perguntar o tipo de ninguém:

fun main() {
    val equipe: List<Funcionario> = listOf(
        Desenvolvedor("Ana", 8000.0),
        Gerente("Bruno", 12000.0, 4),
        Estagiario("Carla", 2000.0),
    )
    for (pessoa in equipe) {
        println("${pessoa.descricao()}: total ${pessoa.total()}")
    }
}
Ana (desenvolvedor): total 8800.0
Bruno (gerente), equipe de 4: total 14000.0
Carla (estagiário): total 2000.0

O laço não conhece Gerente. total() vai para a versão herdada, e bonus() vai para a versão da classe real do objeto. Isso é polimorfismo.

Um laço chama total() em três objetos; cada total() herdado pede o bônus à própria classe, recebe o valor de volta e devolve a soma.
figura 2 · Chamada de total() em três objetos de tipos diferentes

O laço chama total() em cada objeto, e cada um pede bonus() à própria classe. O valor volta rotulado como “retorna”.

8. is, e as

Um laço sobre List<Funcionario> não enxerga tamanhoEquipe, que é propriedade de Gerente. Sozinho num arquivo, este trecho reproduz o erro:

open class Funcionario(val nome: String)

class Gerente(nome: String, val tamanhoEquipe: Int) : Funcionario(nome)

fun main() {
    val equipe: List<Funcionario> = listOf(Gerente("Bruno", 4))
    for (pessoa in equipe) {
        println("${pessoa.nome} coordena ${pessoa.tamanhoEquipe} pessoas")
    }
}
.../src/main/kotlin/Main.kt:8:51 Unresolved reference 'tamanhoEquipe' on receiver of type 'Funcionario'.

A verificação é feita com is. Dentro do if, o compilador trata pessoa como Gerente, o que se chama smart cast. Com as classes da seção 6:

fun main() {
    val equipe: List<Funcionario> = listOf(
        Gerente("Bruno", 12000.0, 4),
        Estagiario("Carla", 2000.0),
    )
    for (pessoa in equipe) {
        if (pessoa is Gerente) {
            println("${pessoa.nome} coordena ${pessoa.tamanhoEquipe} pessoas")
        } else {
            println("${pessoa.nome} não coordena ninguém")
        }
    }
}
Bruno coordena 4 pessoas
Carla não coordena ninguém

O smart cast vale porque pessoa é uma val do laço. A documentação diz que a conversão só é garantida quando a variável não muda entre a verificação e o uso. Para propriedades var, ela diz que o smart cast nunca vale, porque outro código pode mudá-las.

O as converte à força, e é perigoso sem certeza do tipo. Com as classes da seção 6:

fun main() {
    val pessoa: Funcionario = Estagiario("Carla", 2000.0)
    val gerente = pessoa as? Gerente
    println("as?: $gerente")
    val outra = pessoa as Gerente
    println("não chega aqui: ${outra.tamanhoEquipe}")
}
as?: null
Exception in thread "main" java.lang.ClassCastException: class Estagiario cannot be cast to class Gerente (Estagiario and Gerente are in unnamed module of loader 'app')

As linhas at da pilha foram omitidas, porque mudam conforme a posição do trecho no arquivo. Com as?, a falha vira null. Com as, vira ClassCastException e derruba o programa. Sem verificação antes, prefira as?.

9. : o que só a família enxerga

A aula de encapsulamento (Encapsulamento, visibilidade, object e companion) mencionou protected sem usá-lo. Aqui ele tem sentido: salarioBase é protected porque o bônus de cada filha precisa dele, mas o resto do programa não deve lê-lo. A função abaixo, mesmo no mesmo arquivo, não consegue:

open class Funcionario(protected val salarioBase: Double)

fun espiar(f: Funcionario): Double = f.salarioBase

fun main() {
    println(espiar(Funcionario(1.0)))
}
.../src/main/kotlin/Main.kt:3:40 Cannot access 'val salarioBase: Double': it is protected in 'Funcionario'.

A documentação diz que protected vale para a classe e as subclasses, e não existe em declarações de nível superior.

10. quando não usar herança

Herança serve para a relação “é um”. Para só reaproveitar comportamento, a composição costuma bastar: a classe guarda outro objeto numa propriedade e chama os métodos dele. Isso só fica completo com interfaces, assunto da próxima aula da fase. Para um conjunto fechado de tipos, veja sealed e when exaustivo.

11. erros comuns de iniciante

O primeiro erro é herdar para reaproveitar código sem a relação “é um”. Uma Pilha que herda de uma lista ganha a inserção no meio e deixa de ser pilha. Esse exemplo é conceitual e não foi executado aqui.

O segundo é a hierarquia profunda: quatro níveis escondem o comportamento de cada nível atrás do anterior. O domínio desta aula tem dois níveis, e isso basta. Se precisar de mais, pergunte de novo se a relação é “é um”.

O terceiro é o cast sem verificação, já mostrado na seção 8. O quarto é chamar membro aberto no construtor do pai, o caso da seção 5.

Pergunta de entrevistaQuando você escolheria composição no lugar de herança?

Herança liga a subclasse à implementação do pai, e uma mudança no pai pode quebrar os filhos. Quando o reuso é de comportamento e não de tipo, prefiro composição: a classe guarda um objeto que faz o trabalho e delega as chamadas. Por exemplo, um Pedido que usa uma política de frete, em vez de uma subclasse PedidoComFreteExpresso. Herança faz sentido quando existe uma relação de tipo estável e o pai foi projetado para ser estendido.

12. exercícios com resposta

exercício 1: o terceirizado

Adicione a classe Terceirizado, com bônus fixo de 300.0 e cargo “terceirizado”, logo abaixo de Estagiario e herdando de Funcionario. Sobrescreva cargo e bonus() e acrescente Terceirizado("Diego", 3000.0) à lista. A resposta, com a main completa e as classes da seção 6 no mesmo arquivo:

class Terceirizado(nome: String, salarioBase: Double) : Funcionario(nome, salarioBase) {
    override val cargo = "terceirizado"

    override fun bonus(): Double = 300.0
}

fun main() {
    val equipe: List<Funcionario> = listOf(
        Desenvolvedor("Ana", 8000.0),
        Gerente("Bruno", 12000.0, 4),
        Estagiario("Carla", 2000.0),
        Terceirizado("Diego", 3000.0),
    )
    for (pessoa in equipe) {
        println("${pessoa.descricao()}: total ${pessoa.total()}")
    }
}
Ana (desenvolvedor): total 8800.0
Bruno (gerente), equipe de 4: total 14000.0
Carla (estagiário): total 2000.0
Diego (terceirizado): total 3300.0

O laço não mudou.

exercício 2: coordenados com is

Escreva coordenados(equipe: List<Funcionario>): Int, que soma o tamanhoEquipe dos gerentes, usando is e sem as. Acrescente à lista da seção 7 um Gerente("Rafa", 9000.0, 2). A resposta:

fun coordenados(equipe: List<Funcionario>): Int {
    var total = 0
    for (pessoa in equipe) {
        if (pessoa is Gerente) total += pessoa.tamanhoEquipe
    }
    return total
}

fun main() {
    val equipe: List<Funcionario> = listOf(
        Desenvolvedor("Ana", 8000.0),
        Gerente("Bruno", 12000.0, 4),
        Estagiario("Carla", 2000.0),
        Gerente("Rafa", 9000.0, 2),
    )
    println(coordenados(equipe))
}
6

Sem o is, o acesso pessoa.tamanhoEquipe cairia no erro da seção 8.

13. glossário

  • open: modificador que libera uma classe para herança, ou um membro para ser sobrescrito.
  • abstract: classe que não pode ser criada diretamente, ou membro que a filha precisa implementar.
  • override: modificador que marca, na filha, o membro que substitui o do pai.
  • super: referência à superclasse imediata, usada para chamar a versão do pai de um membro.
  • protected: visível na classe e nas subclasses, e fechado para o resto do programa.
  • Polimorfismo: a mesma chamada produz comportamentos diferentes conforme o tipo real do objeto.
  • is: operador que testa o tipo de um objeto e aciona o smart cast quando o teste passa.
  • Smart cast: conversão automática de tipo depois de um is, para valores que não mudam.
  • as: converte o tipo à força e lança ClassCastException se falhar. as? devolve null no lugar da exceção.

14. o que não foi confirmado ou medido

Não testei que protected em Java alcança o pacote, nem o plugin kotlin-spring, citado na aula de interfaces. As regras de smart cast para var e de protected em declarações de nível superior vêm da documentação e não foram executadas. Os trechos rodaram com JDK 25, e o formato do rastro do ClassCastException pode mudar em outras versões.

Fontes

Relacionados

↑ Voltar ao topo

Atalhos de teclado

Abrir busca
/
Alternar tema claro/escuro
t
Aula anterior na série
←
Próxima aula na série
→
Lista de atalhos
?