Programação Java
Herança, polimorfismo e interfaces em Java
Iniciante8 min de leituraAtualizado
Esta página foi traduzida do inglês com a ajuda de IA e conferida com o original. Ler a versão em inglês
O polimorfismo permite que um mesmo ponto de chamada funcione com muitos tipos concretos, e o Java oferece três ferramentas para expressá-lo: herança de classes, classes abstratas e interfaces. Este guia explica como o despacho dinâmico resolve de fato uma chamada, como projetar interfaces com métodos default e static, como classes sealed e pattern matching fecham uma hierarquia com segurança e por que a composição costuma ser a melhor forma de reutilizar código.
O que você vai aprender
- Explicar o que acontece em tempo de execução quando um método sobrescrito é chamado por meio de uma referência do supertipo
- Escolher entre uma classe abstrata e uma interface para um determinado design
- Usar métodos default, static e private em interfaces sem quebrar as classes que as implementam
- Modelar um conjunto fechado de variantes com interfaces sealed e um switch exaustivo
- Reconhecer quando preferir composição a herança
A herança é o recurso mais associado à programação orientada a objetos, e também o mais mal utilizado. Seu verdadeiro ganho não é a reutilização de código — a composição faz isso melhor —, e sim o polimorfismo: escrever código contra um tipo geral e deixar que cada objeto concreto forneça seu próprio comportamento. Este guia cobre os mecanismos que o Java oferece para isso (extensão de classes, classes abstratas, interfaces), as adições modernas que tornam as hierarquias mais seguras (tipos sealed e pattern matching) e os critérios para decidir quando usar cada um. Os exemplos usam Java 21.
Estendendo uma classe
class Manager extends Employee torna Manager uma subclasse que herda todos os campos e métodos acessíveis de Employee e pode adicionar ou substituir comportamento. Um Manager é um Employee, então qualquer código que espera um Employee aceita um Manager. O Java permite uma única superclasse; toda classe sem uma cláusula extends explícita estende java.lang.Object.
public class Employee {
private final String name;
private final long monthlySalaryCents;
public Employee(String name, long monthlySalaryCents) {
this.name = name;
this.monthlySalaryCents = monthlySalaryCents;
}
public long annualPayCents() {
return monthlySalaryCents * 12;
}
public String name() { return name; }
}
public class Manager extends Employee {
private final long bonusCents;
public Manager(String name, long monthlySalaryCents, long bonusCents) {
super(name, monthlySalaryCents); // must run the superclass constructor first
this.bonusCents = bonusCents;
}
@Override
public long annualPayCents() {
return super.annualPayCents() + bonusCents; // reuse, then extend
}
}
Três detalhes desse código importam. O construtor da subclasse chama super(...) como primeira instrução; se você omitir, o compilador insere super(), que não compila quando a superclasse não tem um construtor sem argumentos. A anotação @Override é opcional, mas deveria ser obrigatória na sua base de código: ela transforma em erro de compilação um erro de digitação no nome do método que, sem ela, criaria silenciosamente um método novo. E os campos private são herdados no sentido de que existem no objeto da subclasse, mas não ficam acessíveis ao código dela: Manager chega ao salário pelo método público, não pelo campo.
O que final e protected significam aqui
Marcar uma classe como final impede que ela seja estendida; marcar um método como final impede que ele seja sobrescrito. Use final nas classes que não foram projetadas para extensão — ou seja, a maioria —, porque uma classe extensível precisa garantir que cada método funciona quando qualquer outro é sobrescrito. Um bom design ou documenta exatamente como estender uma classe, ou proíbe que ela seja estendida.
Membros protected são visíveis para as subclasses e para o mesmo pacote. É tentador deixar campos como protected para que as subclasses os usem diretamente, mas isso acopla a subclasse à representação interna da superclasse, e uma mudança futura no campo quebra todas as subclasses. Se as subclasses realmente precisarem desses dados, prefira campos private com métodos de acesso protected.
Despacho dinâmico: como a chamada é resolvida
A regra essencial: o compilador verifica a chamada pelo tipo estático da referência; em tempo de execução, a implementação é escolhida pela classe real do objeto.
Employee e = new Manager("Ada", 800_000, 2_000_000);
long pay = e.annualPayCents(); // compiles because Employee has the method
// runs Manager's override: 11,600,000
O tipo estático Employee decide quais métodos você pode chamar. O tipo dinâmico Manager decide qual corpo é executado. É isso que permite que uma List<Employee> guarde funcionários, gerentes e prestadores de serviço e calcule a folha de pagamento total com um único loop que nunca menciona uma subclasse.
Duas coisas não passam por despacho dinâmico: campos e métodos static. Um acesso a campo como e.someField é resolvido pelo tipo estático, e um método static chamado por meio de uma referência também é resolvido pelo tipo estático (o compilador inclusive emite um aviso quando você chama um método static por uma instância). Se você se pegar querendo "sobrescrever" um método static, o que você quer é um método de instância.
A sobrescrita tem regras que o compilador impõe. O método sobrescrito deve ter o mesmo nome e os mesmos tipos de parâmetros; seu tipo de retorno pode ser um subtipo do original (retorno covariante); ele não pode ser menos acessível que o original; e não pode declarar checked exceptions mais amplas. Essas regras existem para que qualquer chamador escrito contra a superclasse continue funcionando quando receber uma subclasse.
Classes abstratas
Uma classe abstrata não pode ser instanciada e pode declarar métodos abstratos — assinaturas sem corpo que as subclasses concretas precisam implementar. É a ferramenta certa quando várias implementações compartilham estado e parte de um algoritmo, mas diferem em etapas específicas:
public abstract class Report {
private final String title;
protected Report(String title) { this.title = title; }
public final String render() { // template: fixed skeleton
return header() + "\n" + body() + "\n" + footer();
}
protected String header() { return "== " + title + " =="; }
protected String footer() { return "-- end --"; }
protected abstract String body(); // each subclass supplies this
}
public class SalesReport extends Report {
private final long totalCents;
public SalesReport(long totalCents) {
super("Sales");
this.totalCents = totalCents;
}
@Override
protected String body() { return "Total: " + totalCents / 100.0; }
}
Esse é o padrão Template Method: a classe abstrata fixa a sequência e as subclasses preenchem as lacunas. Repare no final em render(): o esqueleto em si não está aberto a renegociação.
Interfaces
Uma interface declara um contrato — um conjunto de assinaturas de métodos — sem estado de instância. Uma classe implementa quantas interfaces quiser, e essa é a resposta do Java à herança múltipla: você pode herdar tipos de muitas fontes, mas implementação de uma única superclasse.
public interface Shape {
double area();
default String describe() { // default method: has a body
return getClass().getSimpleName() + " with area " + area();
}
static Shape unitSquare() { // static method on the interface
return new Rectangle(1, 1);
}
}
public record Rectangle(double width, double height) implements Shape {
@Override public double area() { return width * height; }
}
public record Circle(double radius) implements Shape {
@Override public double area() { return Math.PI * radius * radius; }
}
Os métodos de uma interface são implicitamente public abstract; seus campos são implicitamente public static final, então uma interface não é lugar para guardar estado. Desde o Java 8, uma interface também pode ter:
- Métodos default — implementações herdadas por todas as classes que a implementam, a menos que sejam sobrescritas. Eles existem para que uma interface amplamente implementada possa ganhar um método sem quebrar as classes existentes;
Collection.stream()eIterable.forEach()foram adicionados dessa forma. - Métodos static — métodos utilitários ou de fábrica associados à interface.
- Métodos private (Java 9) — auxiliares compartilhados entre métodos default, invisíveis para quem implementa a interface.
Se uma classe herda dois métodos default com a mesma assinatura de duas interfaces, ela não compila até que a própria classe sobrescreva o método; dentro dele, pode escolher um dos dois com InterfaceName.super.method(). Um método herdado de uma superclasse sempre prevalece sobre um método default de interface, o que mantém estáveis as hierarquias de classes existentes quando métodos default são adicionados.
Interface ou classe abstrata?
| Interface | Classe abstrata | |
|---|---|---|
| Campos de instância | Não | Sim |
| Construtores | Não | Sim |
| Herança múltipla | Sim, quantas quiser | Não, uma única superclasse |
| Corpos de métodos | Apenas default, static e private | Qualquer um |
| Níveis de acesso | Public (ou auxiliares private) | Qualquer um |
| Expressa | Uma capacidade ou um papel | Uma implementação parcial com estado compartilhado |
Comece com uma interface. Se várias implementações acabarem compartilhando campos e código, adicione uma classe abstrata que implemente a interface e faça com que essas implementações a estendam; as demais continuam podendo implementar a interface diretamente. É assim que List e AbstractList se relacionam no JDK.
Uma interface de marcação não tem nenhum método e serve apenas para rotular um tipo — Serializable é o exemplo mais conhecido. As anotações substituíram a maioria dos novos usos dessa ideia, mas você vai encontrá-la em código existente.
Hierarquias sealed e pattern matching
Uma interface tradicional é aberta: qualquer um pode implementá-la, então um código que faz switch sobre "todos os tipos de forma" nunca pode ter sua exaustividade verificada. O Java 17 finalizou as classes e interfaces sealed, que nomeiam seus subtipos permitidos:
public sealed interface PaymentResult permits Approved, Declined, Pending {}
record Approved(String authorisationCode) implements PaymentResult {}
record Declined(String reason) implements PaymentResult {}
record Pending(java.time.Instant retryAfter) implements PaymentResult {}
Cada subtipo permitido precisa ser, ele próprio, final, sealed ou explicitamente non-sealed, e records são implicitamente final, então essa hierarquia está fechada. O pattern matching para switch do Java 21 (JEP 441) permite então que o compilador verifique a exaustividade:
static String message(PaymentResult result) {
return switch (result) {
case Approved a -> "Approved, code " + a.authorisationCode();
case Declined d -> "Declined: " + d.reason();
case Pending p -> "Pending until " + p.retryAfter();
// no default needed: the compiler knows these are all the cases
};
}
Adicione um quarto record a permits e todos esses switches deixam de compilar até tratarem o novo caso. É justamente a segurança que uma cláusula default teria jogado fora. Record patterns (case Approved(String code) ->) desestruturam os componentes diretamente, e guards (case Declined d when d.reason().startsWith("fraud") ->) refinam um caso.
Os tipos sealed afastam alguns designs do polimorfismo clássico. Se o comportamento varia por subtipo e se encaixa naturalmente nele, use um método sobrescrito. Se o comportamento pertence ao consumidor — serialização, renderização, regras de preço que mudam com mais frequência que os tipos —, um switch sobre uma hierarquia sealed mantém essa lógica reunida sem empurrar para dentro do tipo as preocupações de cada módulo.
As antigas verificações com instanceof seguidas de cast também foram simplificadas: if (obj instanceof Circle c) { ... c.radius() ... } vincula a variável de padrão em um só passo (Java 16).
Composição em vez de herança
A herança acopla uma subclasse à implementação da superclasse, não apenas à sua interface. Uma mudança em um método da superclasse pode quebrar uma subclasse que dependia de como ele chamava outros métodos — o problema da "classe base frágil" (fragile base class). A forma mais limpa de reutilizar comportamento é a composição: manter uma referência ao objeto que tem o comportamento e delegar a ele.
// Inheritance: brittle. If HashSet.addAll is later changed to call add,
// or stops calling it, this count silently becomes wrong.
class CountingSet<E> extends HashSet<E> {
private int added;
@Override public boolean add(E e) { added++; return super.add(e); }
@Override public boolean addAll(Collection<? extends E> c) { added += c.size(); return super.addAll(c); }
}
// Composition: robust. Behaviour depends only on Set's public contract.
class CountingSet<E> implements Set<E> {
private final Set<E> inner;
private int added;
CountingSet(Set<E> inner) { this.inner = inner; }
@Override public boolean add(E e) { added++; return inner.add(e); }
@Override public boolean addAll(Collection<? extends E> c) { added += c.size(); return inner.addAll(c); }
// ... every other Set method forwards to inner
}
A versão com composição exige mais digitação (uma classe abstrata de encaminhamento elimina quase tudo isso), mas não pode ser quebrada por detalhes internos de HashSet, funciona com qualquer implementação de Set e pode ser testada com um fake. Use herança quando houver uma relação é-um genuína e a superclasse tiver sido projetada para isso; use composição para relações tem-um e para reutilizar comportamento.
Erros comuns
Chamar métodos sobrescrevíveis a partir de um construtor. A versão sobrescrita da subclasse roda antes de os campos da subclasse serem inicializados, então lê valores padrão (null, 0). Construtores devem chamar apenas métodos private ou final.
Quebrar o contrato da superclasse. Uma sobrescrita que lança exceção onde o pai não lançava, ignora um argumento que o pai respeitava ou enfraquece uma pós-condição viola o princípio de substituição de Liskov: chamadores escritos contra o pai passam a falhar. Se a subclasse não consegue honrar o contrato, ela não é um subtipo.
Hierarquias profundas. Quatro níveis de extends para compartilhar alguns métodos auxiliares são uma armadilha de manutenção. Achate a hierarquia com composição e interfaces.
Esquecer o @Override. Um nome de método digitado errado (equal em vez de equals) compila como um método totalmente novo, e sua sobrescrita nunca é executada.
Cláusulas default em switches sobre tipos sealed. Elas compilam, mas desativam a verificação de exaustividade, que era o objetivo de selar a hierarquia.
Expor campos protected. Eles congelam a representação da superclasse para todas as subclasses existentes.
Confundir cadeias de instanceof com design. Um método que testa instanceof para cinco subtipos e ramifica geralmente indica um método polimórfico que está faltando — ou, se os tipos são seus e formam um conjunto fechado, um switch sobre uma hierarquia sealed.
Principais pontos
- Polimorfismo significa que código escrito contra um supertipo funciona com qualquer subtipo; em tempo de execução, a sobrescrita é escolhida pela classe real do objeto, enquanto o compilador verifica a chamada pelo tipo declarado.
- Campos e métodos static não passam por despacho dinâmico; só métodos de instância.
- Interfaces descrevem capacidades e permitem herança múltipla de tipo; classes abstratas compartilham estado e implementações parciais. Comece com uma interface.
- Métodos default permitem que as interfaces evoluam; uma classe precisa resolver explicitamente conflitos entre métodos default.
- Interfaces sealed combinadas com
switchcom pattern matching oferecem tratamento verificado pelo compilador de um conjunto fechado de variantes — deixe odefaultde fora. - Reutilize comportamento por meio de composição e delegação; reserve a herança para relações é-um genuínas com uma superclasse projetada para extensão.
Fontes e leituras complementares
Perguntas frequentes
- Devo usar uma classe abstrata ou uma interface?
- Use uma interface para descrever uma capacidade que classes sem relação entre si podem compartilhar; uma classe pode implementar várias. Use uma classe abstrata quando as implementações compartilham estado ou uma implementação parcial que precisa de campos, construtores ou membros não públicos. Na dúvida, comece com uma interface: você sempre pode adicionar depois uma classe esqueleto abstrata ao lado dela.
- Por que o Java não suporta herança múltipla de classes?
- Duas superclasses poderiam fornecer campos e implementações conflitantes, sem uma regra clara sobre qual prevalece. Em vez disso, o Java permite que uma classe implemente qualquer número de interfaces e, desde o Java 8, interfaces podem ter métodos default; conflitos entre métodos default precisam ser resolvidos explicitamente pela classe que os implementa.
- Qual a diferença entre sobrescrita (overriding) e sobrecarga (overloading)?
- A sobrescrita substitui um método da superclasse por uma versão da subclasse com a mesma assinatura, escolhida em tempo de execução pela classe real do objeto. A sobrecarga declara, numa mesma classe, vários métodos com o mesmo nome e parâmetros diferentes, e o compilador escolhe entre eles em tempo de compilação com base nos tipos dos argumentos.
- Quando devo usar uma interface sealed?
- Quando o conjunto de implementações é fixo por design e outras partes do código precisam tratar todos os casos: estados de um pedido, nós de uma expressão, tipos de resultado. Selar a interface permite ao compilador verificar se um switch cobre todos os subtipos permitidos, então adicionar uma variante depois gera erros de compilação exatamente onde falta o tratamento.