Como uma classe pode herdar o que outra já tem, mudar só o que precisa e ser usada onde a outra é esperada, e por que toda classe do Java descende de uma mesma raiz.
"O que Fica Dentro, o que Fica Fora" deixou a ContaBancaria sólida: dados protegidos, regras garantidas, extrato e numeração automática. Agora o banco quer dois tipos novos de conta. A conta corrente cobra uma tarifa de R$ 2,50 por saque. A poupança rende juros mensais. Os dois tipos fazem tudo o que a conta comum faz e acrescentam um detalhe. Copiar a classe inteira duas vezes para mudar esse detalhe criaria três cópias das mesmas regras, e cada correção futura teria de ser feita em três lugares. Esta aula apresenta a herança, a forma do Java de criar uma classe a partir de outra, e a classe Object, da qual todas as outras descendem.
extends: herdando de uma classe
A palavra extends declara que uma classe é uma versão especializada de outra:
public class ContaPoupanca extends ContaBancaria {
public ContaPoupanca(String titular, double saldoInicial) {
super(titular, saldoInicial);
}
public void renderJuros(double taxaMensal) {
double juros = getSaldo() * taxaMensal;
if (juros > 0) {
depositar(juros);
}
}
}
ContaPoupanca é a subclasse (ou classe filha), e ContaBancaria é a superclasse (ou classe mãe). A subclasse herda todos os membros da superclasse: uma poupança tem número, titular, saldo, extrato, e os métodos depositar, sacar e getSaldo, sem que nenhuma linha deles tenha sido reescrita. A subclasse só acrescenta o que é novo, o método renderJuros.
Repare que renderJuros usa getSaldo() e depositar(), e não o campo saldo diretamente. Os campos privados da superclasse existem dentro de cada poupança, mas continuam privados: nem a subclasse os acessa. Isso é bom. A subclasse é obrigada a passar pelas mesmas portas que qualquer outro código, e as regras da conta continuam valendo para ela. O rendimento aparece no extrato porque passou por depositar.
super: construindo a parte herdada
Todo objeto de uma subclasse contém, por dentro, a parte da superclasse, e essa parte precisa ser construída. A chamada super(titular, saldoInicial) executa o construtor de ContaBancaria com esses argumentos, com todas as validações, a numeração e a inicialização dos campos. Como o this(...) de "Moldes e Instâncias", ela precisa ser a primeira instrução do construtor.
Se você não chamar super(...), o compilador insere uma chamada ao construtor sem parâmetros da superclasse. Como ContaBancaria não tem um, o erro aparece:
SemSuper.java:2: error: no suitable constructor found for ContaBancaria(no arguments)
É mais uma garantia: não há como criar uma poupança sem passar pelo porteiro da conta.
A construção acontece sempre de cima para baixo. Com uma hierarquia de três níveis:
static class Veiculo {
Veiculo() { IO.println("1. construtor de Veiculo"); }
}
static class Carro extends Veiculo {
Carro() { IO.println("2. construtor de Carro"); }
}
static class Taxi extends Carro {
Taxi() {
super();
IO.println("3. construtor de Taxi");
}
}
new Taxi() imprime:
1. construtor de Veiculo
2. construtor de Carro
3. construtor de Taxi
Cada construtor chama o de cima antes de executar o próprio corpo. A base fica pronta antes da parte especializada, porque a parte especializada pode depender dela. O static antes de class só é necessário porque, neste exemplo, as três classes estão declaradas dentro de outra, as classes aninhadas que aparecem em "Ordem e Critério".
Sobrescrita: mudando um comportamento herdado
A conta corrente não acrescenta um método novo: ela muda o comportamento de um que já existe, o saque, que passa a cobrar tarifa. Para permitir isso de forma controlada, vamos preparar a ContaBancaria com um ponto de extensão, um método que calcula a tarifa e que na conta comum devolve zero:
public boolean sacar(double valor) {
exigirPositivo(valor);
double total = valor + tarifaDeSaque(valor);
if (total > saldo) {
return false;
}
saldo -= total;
registrar(-total);
return true;
}
protected double tarifaDeSaque(double valor) {
return 0.0;
}
Aqui aparece o protected da tabela de "O que Fica Dentro, o que Fica Fora": o método é visível para as subclasses (e para o mesmo pacote), mas não faz parte do que a conta oferece ao público. A conta corrente sobrescreve esse método:
public class ContaCorrente extends ContaBancaria {
private static final double TARIFA_POR_SAQUE = 2.50;
public ContaCorrente(String titular, double saldoInicial) {
super(titular, saldoInicial);
}
@Override
protected double tarifaDeSaque(double valor) {
return TARIFA_POR_SAQUE;
}
}
Sobrescrever (override) é declarar na subclasse um método com a mesma assinatura (nome e tipos dos parâmetros) de um método da superclasse. Quando sacar for chamado numa conta corrente, a chamada interna tarifaDeSaque(valor) executa a versão da conta corrente. A lógica do saque, com a validação, a verificação de saldo e o registro no extrato, continua escrita uma vez só, na superclasse. A subclasse muda apenas a peça que precisa mudar.
Esse desenho, em que a superclasse define o algoritmo e as subclasses preenchem passos específicos, tem nome: Template Method, um dos padrões de projeto clássicos. Ele funciona melhor do que sobrescrever o sacar inteiro, porque a regra do saldo não pode ser esquecida por acidente numa subclasse.
Não confunda sobrescrita com a sobrecarga de "Dividir para Resolver". Sobrecarga é o mesmo nome com parâmetros diferentes, na mesma classe. Sobrescrita é a mesma assinatura, na subclasse, substituindo a versão herdada.
@Override: peça ao compilador para conferir
A anotação @Override declara a intenção de sobrescrever. Ela parece opcional, e tecnicamente é, mas veja o que acontece com um erro de digitação no tipo do parâmetro:
protected double tarifaDeSaque(int valor) { // int, e não double
return 5.0;
}
Sem @Override, isso compila. Não é uma sobrescrita, porque a assinatura é diferente, e sim um método novo, uma sobrecarga que ninguém chama. A tarifa nunca é cobrada, e nada avisa. Com @Override, o compilador recusa:
Typo.java:3: error: method does not override or implement a method from a supertype
@Override
^
Use @Override em toda sobrescrita. Ela transforma um erro silencioso de lógica num erro de compilação. Anotações em geral são assunto de "O Programa que se Examina".
Uma subclasse é uma superclasse
Uma poupança é uma conta bancária. Por isso, uma variável do tipo ContaBancaria pode apontar para um objeto ContaPoupanca ou ContaCorrente:
ContaBancaria comum = new ContaBancaria("Ana", 1000.0);
ContaCorrente corrente = new ContaCorrente("Bruno", 1000.0);
ContaPoupanca poupanca = new ContaPoupanca("Carla", 1000.0);
poupanca.renderJuros(0.005);
ContaBancaria[] contas = {comum, corrente, poupanca};
for (ContaBancaria conta : contas) {
conta.sacar(100.0);
IO.println(conta);
}
#1 Ana: R$ 900,00
#2 Bruno: R$ 897,50
#3 Carla: R$ 905,00 (poupança)
O laço trata as três contas como ContaBancaria e chama o mesmo sacar em todas. Mesmo assim, a conta do Bruno pagou tarifa. A versão de tarifaDeSaque executada é decidida durante a execução, pelo tipo real do objeto, e não pelo tipo da variável. Esse mecanismo, chamado de despacho dinâmico, é a base do polimorfismo, e a aula "Um Tipo, Várias Formas" é dedicada a ele. A linha da Carla, com o "(poupança)" no fim, é a mesma ideia aplicada ao toString, explicado mais adiante nesta aula.
O tipo da variável ainda importa para uma coisa: o que o compilador deixa você chamar. Pela variável de tipo ContaBancaria, só os métodos de ContaBancaria estão disponíveis, mesmo que o objeto seja uma poupança:
ContaBancaria c = new ContaPoupanca("Dani", 10);
c.renderJuros(0.01);
E1.java:4: error: cannot find symbol
c.renderJuros(0.01);
^
symbol: method renderJuros(double)
O compilador só sabe que c é alguma conta, e nem toda conta rende juros. É o tipo da variável que determina o que pode ser chamado, e o tipo do objeto que determina qual versão executa. Guarde essas duas regras juntas, porque elas explicam quase tudo sobre polimorfismo.
Object: a raiz de tudo
Uma classe sem extends herda, implicitamente, de java.lang.Object. Por isso, toda classe do Java descende de Object, direta ou indiretamente. ContaCorrente estende ContaBancaria, que estende Object:
Object o = new ContaBancaria("Eva", 50);
IO.println(o.getClass().getName()); // ContaBancaria
IO.println(o.getClass().getSuperclass().getName()); // java.lang.Object
Qualquer objeto pode ser guardado numa variável Object, inclusive textos e números: Object texto = "oi"; funciona, e Object numero = 42; também, com o 42 embrulhado automaticamente num objeto Integer, um mecanismo que aparece em "Escreva uma Vez para Todo Tipo".
Object define os métodos que todo objeto tem. Os mais importantes são:
toString(): devolve um texto que descreve o objeto. A versão herdada devolve o nome da classe e um código, oContaBancaria@7d68ef40que você viu em "Moldes e Instâncias".equals(Object)ehashCode(): definem quando dois objetos são considerados iguais. A versão herdada compara identidade, como o==. Sobrescrevê-los corretamente é delicado e tem uma aula própria, "Iguais ou o Mesmo?".getClass(): devolve o tipo real do objeto durante a execução.
O toString é chamado automaticamente sempre que um objeto é concatenado com texto ou passado para IO.println. Sobrescrevê-lo substitui o antigo método resumo():
@Override
public String toString() {
return String.format(Locale.of("pt", "BR"), "#%d %s: R$ %,.2f", numero, titular, saldo);
}
E a poupança reaproveita a versão da mãe, acrescentando um detalhe:
@Override
public String toString() {
return super.toString() + " (poupança)";
}
super.toString() chama a versão da superclasse. É a forma de estender um comportamento herdado em vez de substituí-lo por completo: aproveitar o que a mãe faz e somar alguma coisa.
final: fechando a porta
Nem toda classe deve poder ser estendida. O final na declaração de uma classe proíbe subclasses:
public class Fin extends String { }
Fin.java:1: error: cannot inherit from final String
O String é final justamente para proteger a imutabilidade de "O que Fica Dentro, o que Fica Fora". Se fosse possível criar uma subclasse de String com um campo mutável, um objeto dessa subclasse poderia ser passado a qualquer código que espera um String e mudar depois, e todas as garantias desapareceriam. Pela mesma razão, a classe Ponto da aula anterior foi declarada final.
Um método também pode ser final, e então não pode ser sobrescrito. Na ContaBancaria, sacar e depositar são candidatos naturais: são eles que garantem as regras da conta, e uma subclasse que os sobrescrevesse poderia contornar tudo. O ponto de extensão planejado é tarifaDeSaque. O sacar pode ficar fechado.
Herança: poderosa e perigosa
O Java permite herdar de uma única classe. class Multi extends ContaCorrente, ContaPoupanca nem chega a ser entendido pelo compilador. A herança múltipla de classes, permitida em C++, traz ambiguidades difíceis (o que fazer quando as duas mães têm o mesmo método?), e o Java preferiu evitá-las. A próxima aula mostra a alternativa: uma classe pode implementar vários contratos.
A herança é uma das ferramentas mais poderosas da orientação a objetos e também uma das mais mal usadas. Dois conselhos, que a experiência de décadas consolidou:
Use herança só quando a relação "é um" for verdadeira em todos os sentidos. Uma poupança é uma conta, e tudo o que se pode fazer com uma conta se pode fazer com uma poupança. O teste mais rigoroso é o princípio da substituição de Liskov: um objeto da subclasse deve poder ser usado em qualquer lugar onde se espera a superclasse, sem que o programa se comporte de forma incorreta. Se a subclasse precisa recusar operações da mãe ou mudar o significado delas, a relação está errada, mesmo que pareça natural na linguagem do dia a dia. O exercício 5 mostra um exemplo clássico.
Prefira composição quando a relação for "tem um". Um carro tem um motor, e não é um motor. Uma classe que só quer reaproveitar código de outra, sem ser de fato uma versão dela, deve guardar um objeto da outra como campo e delegar a ele, em vez de herdar. A herança acopla a subclasse aos detalhes da mãe: uma mudança interna na superclasse pode quebrar subclasses que dependiam dela. Esse problema tem até nome, classe base frágil. Planejar a extensão, como fizemos com tarifaDeSaque, e fechar o resto com final reduz esse risco.
Mas repare numa coisa estranha no desenho atual: a ContaBancaria comum, que deveria ser só o tronco comum, também pode ser criada com new. Num banco em que toda conta é corrente ou poupança, uma "conta genérica" não deveria existir. E há situações em que classes sem nenhum parentesco precisam oferecer a mesma operação, como um e-mail e um SMS, que sabem "enviar uma mensagem". As duas situações pedem a mesma ideia, a de um contrato que diz o que fazer sem dizer como.
Fontes e leituras recomendadas
- The Java Language Specification, Java SE 25, seção 8.4.8, "Inheritance, Overriding, and Hiding": https://docs.oracle.com/javase/specs/jls/se25/html/jls-8.html#jls-8.4.8. As regras exatas de herança e sobrescrita, inclusive as restrições sobre tipos de retorno e modificadores de acesso.
- Documentação da classe
Object(Java SE 25): https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/lang/Object.html. Os métodos que todo objeto herda, com os contratos que as sobrescritas devem respeitar. - Joshua Bloch, Effective Java, 3ª edição, itens 18 e 19: "Favor composition over inheritance" e "Design and document for inheritance or else prohibit it". Os dois itens mais citados sobre os riscos da herança.
- Barbara Liskov e Jeannette Wing, "A Behavioral Notion of Subtyping" (1994): https://dl.acm.org/doi/10.1145/197320.197383. O artigo que formalizou o princípio da substituição. É técnico, mas a introdução é acessível.
- Erich Gamma, Richard Helm, Ralph Johnson e John Vlissides, Padrões de Projeto (Bookman): o livro clássico dos padrões de projeto, onde o Template Method está descrito. A introdução discute herança e composição e é leitura obrigatória sobre o tema.
Exercícios
Exercício 1
Sem rodar, diga o que o trecho imprime, supondo as classes da aula (a conta corrente cobra R$ 2,50 por saque).
ContaBancaria a = new ContaCorrente("Ana", 100);
ContaBancaria b = new ContaBancaria("Bia", 100);
a.sacar(97.50);
b.sacar(97.50);
boolean ok = a.sacar(1);
IO.println(a.getSaldo() + " " + b.getSaldo() + " " + ok);
Ver resposta
✓ Resposta: Imprime 0.0 2.5 false. a aponta para uma conta corrente. Mesmo sendo uma variável do tipo ContaBancaria, o saque executa a tarifa da conta corrente: 97,50 + 2,50 = 100, e o saldo zera. b é uma conta comum, sem tarifa: sobram 2,50. O segundo saque de a precisaria de 1 + 2,50 = 3,50, mais do que o saldo zero, e é recusado. É o despacho dinâmico: o tipo da variável é o mesmo nos dois casos, mas o comportamento segue o tipo real de cada objeto.
Exercício 2
Explique por que, sem a anotação @Override, o método tarifaDeSaque(int valor) da aula compila e a tarifa nunca é cobrada.
Ver resposta
✓ Resposta: A assinatura de um método inclui os tipos dos parâmetros. tarifaDeSaque(int) e tarifaDeSaque(double) são métodos diferentes, então a subclasse não sobrescreveu o método da mãe: ganhou um método novo, que sobrecarrega o nome. O sacar da superclasse chama tarifaDeSaque(valor) com um double, e só a versão com double é compatível com essa chamada. Por isso executa sempre a versão herdada, que devolve zero. O programa compila e roda sem nenhum erro aparente, e o defeito só aparece quando alguém percebe que a tarifa não está sendo cobrada. Com @Override, o compilador verifica que o método realmente sobrescreve algo e recusa a versão com int, transformando um erro de lógica silencioso num erro de compilação.
Exercício 3
Acrescente um IO.println ao construtor de ContaBancaria ("criando conta") e outro ao de ContaPoupanca ("criando poupança"). Preveja a ordem das mensagens em new ContaPoupanca("Gil", 10). Seria possível inverter essa ordem?
Ver resposta
✓ Resposta: Aparece primeiro "criando conta" e depois "criando poupança". O construtor de ContaPoupanca começa com super(titular, saldoInicial), que executa todo o construtor de ContaBancaria antes de voltar ao corpo da subclasse. Não é possível inverter: o Java exige que a chamada ao construtor da superclasse aconteça antes de o corpo do construtor usar o objeto. A razão é de segurança. A parte herdada precisa estar pronta e válida antes de a subclasse trabalhar sobre ela. Se a subclasse pudesse executar código antes, poderia chamar getSaldo() sobre uma conta ainda não inicializada. Versões recentes do Java passaram a permitir algumas instruções antes do super(...), desde que não usem o objeto em construção, por exemplo para validar argumentos. A ordem da construção em si continua a mesma.
Exercício 4
Uma variável do tipo ContaBancaria aponta para uma poupança, e é preciso render juros. Por que conta.renderJuros(0.01) não compila, e qual seria a melhor forma de organizar o código para não precisar dessa chamada?
Ver resposta
✓ Resposta: O compilador decide o que pode ser chamado pelo tipo da variável, e ContaBancaria não tem renderJuros. Ele não pode supor que o objeto é uma poupança, porque a mesma variável poderia apontar para uma conta corrente. Existe uma forma de perguntar o tipo real e converter a referência, com instanceof e cast, que aparece em "Um Tipo, Várias Formas" e, em versão moderna, em "Hierarquias Fechadas". Mas a melhor solução costuma ser não perder a informação do tipo: se aquele trecho do programa lida com poupanças, a variável deve ser do tipo ContaPoupanca, e a chamada compila. Usar o tipo mais genérico só faz sentido onde o código realmente trata todas as contas da mesma forma, como no laço de saques da aula.
Exercício 5
Um programador cria class Quadrado extends Retangulo, usando uma versão mutável de Retangulo com métodos setLargura e setAltura. No Quadrado, ele sobrescreve os dois para alterarem largura e altura juntas, e assim manter o quadrado quadrado. Mostre um código que funciona com retângulos e quebra com quadrados, e explique qual princípio foi violado.
Ver resposta
✓ Resposta:
void dobrarLargura(Retangulo r) {
double alturaAntes = r.getAltura();
r.setLargura(r.getLargura() * 2);
// Para qualquer retângulo, a altura não deveria ter mudado:
if (r.getAltura() != alturaAntes) {
IO.println("A altura mudou sozinha!");
}
}
Com um Retangulo 2 × 3, a largura vai para 4 e a altura continua 3, como esperado. Com um Quadrado 2 × 2, setLargura(4) também muda a altura para 4, a mensagem aparece e a área, que deveria ter dobrado, quadruplicou. Todo código escrito para retângulos, que pressupõe que largura e altura mudam de forma independente, passa a se comportar errado quando recebe um quadrado. É uma violação do princípio da substituição de Liskov: o quadrado não pode substituir o retângulo sem alterar o comportamento correto do programa. Na geometria, "todo quadrado é um retângulo" é verdade. No código, com retângulos mutáveis, não é, porque os comportamentos não são compatíveis. Com a versão imutável do exercício 3 de "O que Fica Dentro, o que Fica Fora", o problema desaparece, já que não há setters para entrar em conflito. É mais um argumento a favor da imutabilidade.