1. Introducao
Na aula passada, aprendemos a sobrescrever metodos e propriedades com override e a abrir os pontos de extensao necessarios com open; tambem conhecemos protected como “acessivel a classes derivadas, mas nao ao mundo externo”. Hoje, vamos adicionar uma habilidade pratica importante: como nao quebrar a logica base ao sobrescrever um comportamento e por que, as vezes, super nao significa “eu desisti”, e sim “eu fiz do jeito certo”. E ainda vamos analisar a ordem de inicializacao, por causa da qual iniciantes frequentemente sentem que “o Kotlin esta me trollando”.
Quando voce sobrescreve um metodo, aparece uma tentacao: “pronto, agora eu mando; a classe base pode se aposentar”. Mas, em codigo real, a classe base frequentemente faz algo importante: valida entradas, normaliza texto, registra logs, calcula alguns campos, mantem invariantes. Se voce “cortar” esse comportamento, o programa pode comecar a agir de forma estranha — e sem nenhum erro de compilacao. E e ai que super entra como uma pessoa educada que diz: “primeiro fazemos a parte base, depois eu adiciono a minha”.
Em Kotlin, a palavra-chave super permite, a partir de um membro sobrescrito, acessar a implementacao da classe base (do metodo ou do acessor de propriedade). A formulacao oficial costuma soar mais ou menos assim: “o codigo na classe derivada pode chamar a implementacao da superclasse via super”.
A ideia mais importante: super nao e necessario sempre, mas, se a classe base executa passos obrigatorios, super e a forma de estender, e nao de quebrar.
2. super em um metodo sobrescrito
Normalmente a vida nao pede “substitua o metodo inteiro”, e sim “faca a mesma coisa, mas com um extra”. Por exemplo: a classe base ja sabe imprimir uma mensagem, e a derivada quer adicionar um prefixo. Ou a classe base valida uma string, e a derivada adiciona mais uma verificacao — mas ainda quer usar a validacao base.
Primeiro, vamos ver um exemplo minimo para pegar a intuicao:
open class Logger {
open fun log(message: String) {
println("[LOG] $message")
}
}
class TaggedLogger : Logger() {
override fun log(message: String) {
super.log("TAG: $message")
}
}
fun main() {
val logger = TaggedLogger()
logger.log("Hello")
// [LOG] TAG: Hello
}
Aqui, a classe derivada nao reinventa a impressao: ela reutiliza o “como exatamente exibir o log” e so adiciona a sua parte.
Agora vamos levar a ideia para o nosso contexto de estudo (um aplicativo de console para controle de gastos). Imagine que queremos imprimir “linhas de relatorio”. A classe base e responsavel pelo formato geral (por exemplo, identacao e simbolos consistentes), e as derivadas adicionam o conteudo especifico.
open class ReportLine {
open fun render(): String = "- (empty)"
}
class TextLine(private val text: String) : ReportLine() {
override fun render(): String = super.render() + " $text"
}
fun main() {
val line = TextLine("hello")
println(line.render())
// - (empty) hello
}
O exemplo e propositalmente “bobalhao”, mas ele mostra a mecanica: super.render() e a parte base do formato; depois adicionamos a nossa. Na vida real, o render() base vai fazer algo mais util do que escrever "empty", e e justamente por isso que vale a pena estender via super, em vez de copiar o formato para cada derivado.
Uma versao um pouco mais realista: a classe base garante que a linha do relatorio nunca seja vazia (por exemplo, retorna "(no data)"), e a derivada adiciona detalhes sem esquecer esse contrato.
3. super e propriedades: acesso ao getter
Para iniciantes, muitas vezes parece que propriedade e “apenas um campo”, e metodo e “logica”. Em Kotlin, essa e uma ilusao perigosa: propriedade quase sempre significa getter (e as vezes setter), isto e, “um pequeno metodo por baixo dos panos”. Por isso, super.someProp nao e magia: e “chame a implementacao do getter da classe base”.
Na documentacao sobre heranca isso costuma ser mostrado assim: a classe derivada pode acessar a propriedade da superclasse via super.property e usa-la como parte da sua logica.
Exemplo:
open class Document {
open val title: String = "Untitled"
}
class NamedDocument(private val name: String) : Document() {
override val title: String
get() = super.title + ": " + name
}
fun main() {
val doc = NamedDocument("Budget report")
println(doc.title)
// Untitled: Budget report
}
Repare no detalhe: nos sobrescrevemos o val title nao como um “valor armazenado”, mas via get(). Isso e pratico quando o “valor” precisa ser calculado, e nao guardado. E sim, super.title aqui chama o getter base.
Em projetos reais, isso aparece bastante para “estender uma descricao”. A classe base guarda um trecho comum; a derivada adiciona um sufixo. E se, na classe base, a descricao tambem for calculada (e nao apenas armazenada), super ajuda a reutilizar essa logica base.
4. Ordem de inicializacao na heranca
Antes de chegarmos aos pontos traiçoeiros, vamos entender em que ordem um objeto e criado quando existe uma classe base e uma derivada. Muitos bugs de heranca parecem “por que meu campo ainda e 0/null, se eu ja atribui?!”, e quase sempre a resposta e a ordem de inicializacao.
A regra intuitiva e: ao criar um objeto da classe derivada, primeiro e criada a “parte base” do objeto, e so depois a “parte da derivada”. Isso faz sentido, mas ha um detalhe importante: antes da inicializacao da classe base, ainda sao avaliados os argumentos para chamar o construtor dela. Em Kotlin, isso e descrito de forma direta: a inicializacao base acontece primeiro (tirando a avaliacao dos argumentos do construtor da superclasse), e isso significa que as propriedades sobrescritas na derivada ainda nao foram inicializadas quando o codigo do construtor base ou do init e executado.
Vamos ver um exemplo demonstrativo imprimindo as etapas. Aqui, de proposito, usamos also { println(...) } para mostrar “em que momento cada coisa foi avaliada”:
open class Base(name: String) {
init {
println("Base.init, name=$name")
}
open val size: Int = name.length.also {
println("Base.size initialized: $it")
}
}
class Derived(name: String, private val lastName: String) : Base(
name.uppercase().also { println("Argument for Base: $it") }
) {
init {
println("Derived.init, lastName=$lastName")
}
override val size: Int = (super.size + lastName.length).also {
println("Derived.size initialized: $it")
}
}
fun main() {
println("Constructing...")
Derived("hello", "world")
}
Saida esperada (mais ou menos assim; a ordem importa mais do que o texto exato):
Constructing...
Argument for Base: HELLO
Base.init, name=HELLO
Base.size initialized: 5
Derived.init, lastName=world
Derived.size initialized: 10
Se tirarmos um “esqueleto” do que esta acontecendo, fica este fluxo:
flowchart TD
A["Criamos Derived(...)"] --> B["Avaliamos os argumentos para Base(...)"]
B --> C["Inicializacao de Base: propriedades + blocos init"]
C --> D["Inicializacao de Derived: propriedades + blocos init"]
E por isso que a classe base precisa ser cautelosa: durante a inicializacao dela, a derivada ainda nao esta “montada”.
Alias, se voce quiser checar rapidamente a ordem no seu proprio codigo, um println dentro de init { } e uma ferramenta de debug honesta. O Kotlin nao julga (o seu futuro lider de equipe vai julgar um pouco, mas isso ja e outra historia).
5. Por que chamadas open em init sao perigosas
Agora chegamos ao ponto em que heranca deixa de ser um gatinho fofo e vira um gatinho que derruba a caneca da mesa porque “e o que voce vai fazer a respeito”. O problema e o seguinte: se a classe base, dentro de init, chama um metodo open ou acessa uma propriedade open, o Kotlin pode chamar a sobrescrita na classe derivada, mesmo que a derivada ainda nao tenha inicializado seus campos.
Isso nao e uma “peculiaridade do compilador”; e uma consequencia logica do polimorfismo: a chamada precisa ir para a implementacao mais especifica. A documentacao alerta explicitamente: ao projetar uma classe base, vale evitar usar membros open em construtores, inicializadores de propriedades e blocos init.
Vamos ver um exemplo que parece inocente, mas pode quebrar:
open class BaseGreeter {
init {
// Bad idea: the open method will be called at a time
// when the derived class may not be ready yet.
println(buildGreeting())
}
open fun buildGreeting(): String = "Hello from Base"
}
class FancyGreeter : BaseGreeter() {
private val prefix: String = "[Fancy]"
override fun buildGreeting(): String = "$prefix Hello from Fancy"
}
fun main() {
FancyGreeter()
}
A primeira vista, parece tudo ok: prefix foi declarado. Mas a ordem faz com que BaseGreeter.init rode antes da inicializacao de FancyGreeter (suas propriedades e init). Em alguns cenarios, isso faz com que a sobrescrita tente usar um estado que ainda nao esta pronto.
Para deixar o problema o mais claro possivel e garantir o erro, vamos usar lateinit. Isso e uma situacao real: voce adia a inicializacao, mas, se houver uma chamada precoce, vai receber uma excecao.
open class BaseFormatter {
init {
println(format()) // risk
}
open fun format(): String = "(base)"
}
class UserFormatter : BaseFormatter() {
private lateinit var userName: String
init {
userName = "Alice"
}
override fun format(): String = "User=$userName"
}
fun main() {
UserFormatter()
// UninitializedPropertyAccessException: lateinit property userName has not been initialized
}
Esse e exatamente o momento “por que isso foi parar na derivada?”. Foi parar porque o metodo e open, e o objeto real e UserFormatter.
Como corrigir? A ideia e simples: a classe base nao deve depender de sobrescritas para conseguir se inicializar. Um padrao seguro e ter, na classe base, um metodo final chamado no init, e colocar a extensao em um ponto separado que voce chama depois que o objeto com certeza esta pronto.
Por exemplo: o init base imprime apenas a parte base; e o “extra” voce pode chamar depois de criar o objeto (a partir de codigo externo ou do init da derivada, quando ela ja esta pronta).
open class SafeBaseFormatter {
init {
println(baseFormat()) // base part, not open
}
fun baseFormat(): String = "(base safe)"
}
class SafeUserFormatter : SafeBaseFormatter() {
private val userName: String = "Alice"
fun fullFormat(): String = "User=$userName"
}
fun main() {
val f = SafeUserFormatter()
println(f.fullFormat()) // User=Alice
}
Sim, isso e um pouco menos “magico”, mas e previsivel. E previsibilidade e praticamente a principal moeda na programacao (depois do cafe).
6. Pequeno passo no projeto: formatacao de linhas do relatorio
Para fixar o tema sem ficar em “retangulos abstratos”, vamos encaixar super em um pequeno trecho do nosso aplicativo de console de controle de gastos. Imagine que ja temos uma lista de despesas e imprimimos um relatorio. Queremos que todas as linhas do relatorio tenham um estilo comum: identacao, marcador, limite de comprimento, e que as derivadas adicionem detalhes sem copiar esse estilo manualmente.
Vamos fazer uma classe base ReportLine que sabe gerar o “cabecalho da linha”, e as derivadas adicionam o conteudo:
open class ReportLine {
protected fun bullet(): String = "•"
open fun render(): String = "${bullet()} "
}
class TextReportLine(private val text: String) : ReportLine() {
override fun render(): String = super.render() + text
}
fun main() {
println(TextReportLine("Expenses").render())
// • Expenses
}
Aqui ja da para ver duas ideias do dia: super.render() adiciona a parte comum, e protected deixa o metodo “de servico” bullet() acessivel a derivadas, mas nao ao mundo externo.
Agora vamos adicionar uma linha de “total”, em que queremos reforcar o formato: a parte base continua igual para todos, mas a palavra "TOTAL" e adicionada. Aqui eu mostro de proposito que da para chamar super ate no meio da string — e uma funcao como qualquer outra.
class TotalReportLine(private val total: Int) : ReportLine() {
override fun render(): String = super.render() + "TOTAL = $total"
}
fun main() {
println(TotalReportLine(1200).render())
// • TOTAL = 1200
}
E agora um exemplo em que super e usado em propriedades, porque “titulo” tambem fica bom como propriedade:
open class TitledLine {
open val title: String = "line"
open fun render(): String = "[$title]"
}
class CategoryLine(private val category: String) : TitledLine() {
override val title: String
get() = super.title + ":" + category
}
fun main() {
println(CategoryLine("Food").render())
// [line:Food]
}
Aqui, super.title acessa o getter base. Mesmo que o title base passe a ser calculado, o codigo da derivada nao precisa ser reescrito.
E a moral pratica: super ajuda a garantir que o estilo comum do relatorio viva em um lugar so. Quando voce decidir trocar o marcador "•" por "-", nao vai precisar procurar 15 copias de formatacao espalhadas pelo projeto.
Quando chamar super e quando nao
Na pratica, “sempre chame super” e um conselho ruim, e “nunca chame super” tambem. Voce precisa de um criterio: a implementacao base faz parte de um contrato obrigatorio, ou voce realmente esta substituindo a logica?
E util manter esta tabelinha na cabeca:
| Situacao | O que geralmente significa | O que fazer com mais frequencia |
|---|---|---|
| O metodo base valida dados, registra log, atualiza um contador | Infraestrutura importante; sem isso, a classe pode ficar “quebrada” | Chamar super e adicionar a sua parte antes/depois |
| O metodo base retorna um “placeholder” ou um padrao | A base nao carrega significado; e apenas “para existir” | Voce pode substituir totalmente sem super |
| A sobrescrita muda o formato, mas o esqueleto geral e o mesmo | A parte comum e necessaria para todas as derivadas | super + complemento |
| A classe base chama um metodo open dentro de init | Risco de chamada precoce na derivada | Refatorar o design: super nao resolve aqui |
E uma regra separada, que vale escrever em um post-it: se a classe base faz algo importante para a corretude e voce nao chamou super, seu codigo pode funcionar “quase sempre” — e esse e o modo de falha mais traiçoeiro.
7. Erros tipicos
Erro nº 1: “Eu sobrescrevi o metodo — entao a logica base nao e mais necessaria”.
Esse tipo de abordagem frequentemente quebra invariantes da classe base: por exemplo, ela normaliza dados de entrada, mantem um contador, preenche campos auxiliares ou imprime uma parte obrigatoria do relatorio. Se voce nao chama super, voce nao esta apenas “mudando o comportamento”; voce pode cancelar sem querer uma parte importante do contrato.
Erro nº 2: chamar super “no lugar errado”, e a logica fica estranha.
As vezes a ordem importa: o metodo base pode assumir que sua chamada e o primeiro passo (por exemplo, validacao de argumentos) ou, ao contrario, o ultimo passo (por exemplo, finaliza o estado). Se voce chama super depois de ja ter feito acoes que dependem da preparacao base, voce ganha bugs do tipo “parece tudo certo, mas o resultado fica torto”.
Erro nº 3: acessar super.someProperty esperando que seja apenas um campo, e nao um calculo.
Em Kotlin, propriedade e, na pratica, metodos get()/set(). super.title pode ser calculado, pode acessar outros campos e ate ter efeitos colaterais (embora isso nao seja recomendado). Por isso, super.property deve ser visto como chamada de logica, e nao como leitura de memoria.
Erro nº 4: chamar metodos/propriedades open a partir do init da classe base.
Esse e um dos bugs “profissionais” mais desagradaveis: o codigo compila, as vezes ate roda, mas quando a classe derivada fica mais complexa, comeca a falhar porque os campos “ainda nao estao prontos”. O Kotlin alerta separadamente sobre esse padrao: ao projetar a classe base, e melhor evitar chamadas open no construtor e no init.
Erro nº 5: tentar resolver a ordem de inicializacao com “mais um lateinit”.
lateinit e uma ferramenta util, mas nao torna a ordem de inicializacao mais segura. Pelo contrario: se a chamada open acontecer cedo demais, voce vai receber uma excecao. O tratamento correto nao e “adiar ainda mais a inicializacao”, e sim garantir que a classe base nao dependa de sobrescritas para ser criada, levando em conta a ordem de inicializacao na hierarquia.
GO TO FULL VERSION