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
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.
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.
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
ClassCastExceptionse falhar.as?devolvenullno 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
- Kotlin, herança: https://kotlinlang.org/docs/inheritance.html (consultada em 2026-11-04).
- Kotlin, classes: https://kotlinlang.org/docs/classes.html (consultada em 2026-11-04).
- Kotlin, modificadores de visibilidade: https://kotlinlang.org/docs/visibility-modifiers.html (consultada em 2026-11-04).
- Kotlin, conversões de tipo: https://kotlinlang.org/docs/typecasts.html (consultada em 2026-11-04).
- Erros e saídas executados com Kotlin 2.4.21, Gradle 9.7.0 e JDK 25, em 2026-11-04.