Sobrescrita de Métodos em Java
Sobrescreva métodos da classe pai em subclasses Java com a anotação @Override e despacho dinâmico.
Sobrescrita ocorre quando uma subclasse declara um método com a mesma assinatura de um herdado, substituindo a versão do pai. Combinada com polimorfismo, é o mecanismo que permite chamar um método em uma referência do tipo pai e ter a implementação correta da subclasse sendo executada.
Este capítulo define as regras: o que conta como sobrescrita, o que não conta, o que o compilador permite alterar na versão da subclasse e o que @Override realmente oferece.
Como é uma sobrescrita
Um método sobrescreve um herdado quando todos estes requisitos são satisfeitos:
- Mesmo nome.
- Mesma lista de parâmetros (tipos e ordem, após o apagamento de genéricos).
- Tipo de retorno igual ao do pai ou um subtipo dele (retorno covariante).
- Visibilidade igual à do pai ou mais ampla.
- Não declarado como
final,staticouprivateno pai.
class Animal {
String speak() { return "(noise)"; }
}
class Cat extends Animal {
@Override
String speak() { return "meow"; }
}Esta é uma sobrescrita válida. Chamar speak() em um Cat retorna "meow"; chamá-lo por meio de uma referência Animal que aponta para um Cat também retorna "meow" — o despacho é decidido pela classe do objeto real.
@Override — use sempre
@Override é uma anotação que diz ao compilador "pretendo que isto sobrescreva um método herdado." Se não sobrescrever de fato — nome errado, parâmetros errados, tipo de retorno errado — o compilador emite um erro:
class Cat extends Animal {
@Override
String Speak() { return "meow"; } // ERROR — no Speak() in Animal
}Sem a anotação, isso seria compilado silenciosamente como um método novo chamado Speak, e ((Animal)cat).speak() continuaria retornando o "(noise)" do pai. A anotação não tem custo e detecta toda uma família de bugs silenciosos. Sempre escreva-a nas sobrescritas.
As sobrescritas que você mais escreverá não são das suas próprias classes — são de métodos herdados de Object, a raiz de toda classe. Sobrescrever toString(), equals(Object) e hashCode() é como seus objetos se imprimem de forma legível, comparam por valor e se comportam corretamente como chaves em um HashMap ou HashSet. @Override nesses métodos detecta o bug clássico de escrever equals(Cat other) (uma sobrecarga) em vez de equals(Object other) (a sobrescrita real).
Sobrescrita vs sobrecarga
| Aspecto | Sobrescrita | Sobrecarga |
|---|---|---|
| Onde | Entre um par subclasse / superclasse | Na mesma classe |
| Assinatura | Deve corresponder à do pai | Deve diferir das demais |
| Despacho | Em tempo de execução, baseado no objeto real | Em tempo de compilação, baseado nos tipos dos argumentos |
| Anotação | @Override | nenhuma |
Esses conceitos são frequentemente confundidos. Sobrescrita é um método, vários tipos de objeto. Sobrecarga é vários métodos, um tipo, escolhido pelo que você passa.
Tipos de retorno covariantes
A subclasse pode declarar um tipo de retorno mais restrito que o pai:
class Animal {
Animal makeBaby() { return new Animal(); }
}
class Cat extends Animal {
@Override
Cat makeBaby() { return new Cat(); } // returns Cat, not Animal — fine
}Isso é covariância — o tipo de retorno da sobrescrita é um subtipo do pai. É seguro porque qualquer chamador que espera um Animal de makeBaby() aceita tranquilamente um Cat. O benefício é que chamadores que sabem que têm um Cat recebem um Cat de volta sem conversão de tipo:
Cat kitten = new Cat().makeBaby(); // no cast neededFazer o contrário (uma subclasse ampliando o tipo de retorno) não é permitido — isso quebraria os chamadores que esperavam o tipo mais restrito.
A visibilidade só pode ser ampliada
A sobrescrita deve ter ao menos a mesma visibilidade do método do pai:
class A { public void hi() { } }
class B extends A { protected void hi() { } } // ERROR — reduces visibility
class C extends A { public void hi() { } } // okRestringir a visibilidade significaria que um chamador com uma referência A poderia chamar hi(), mas, após a atribuição A a = new B(), não poderia mais — o que quebraria a substitutibilidade.
As exceções só podem diminuir
Se o método pai não declara exceções verificadas, a sobrescrita também não pode declarar nenhuma. Se o pai lança uma exceção verificada, a sobrescrita pode lançar a mesma ou uma subclasse mais específica dela — mas não uma mais ampla:
class A {
void run() throws IOException { ... }
}
class B extends A {
@Override void run() throws FileNotFoundException { ... } // ok — FileNotFoundException extends IOException
}
class C extends A {
@Override void run() throws Exception { ... } // ERROR — too broad
}Exceções não verificadas (subclasses de RuntimeException) são irrestritas — você sempre pode lançá-las a partir de uma sobrescrita.
O que não pode ser sobrescrito
- Métodos
private. Eles não são visíveis para a subclasse, então um método com o mesmo nome na subclasse é simplesmente um novo método. - Métodos
final. Marcados especificamente para impedir sobrescritas. - Métodos
static. Uma subclasse pode declarar um método estático com o mesmo nome e assinatura, mas isso é chamado de ocultamento de método, não sobrescrita. Não há polimorfismo — a JVM resolve a chamada com base no tipo em tempo de compilação da referência:
class A { static String klass() { return "A"; } }
class B extends A { static String klass() { return "B"; } }
A a = new B();
System.out.println(a.klass()); // "A" — static, not polymorphicEste é um dos poucos lugares onde a regra "o despacho de método escolhe a versão do objeto real" não se aplica.
Chamando o pai a partir da sobrescrita
Um padrão comum é estender, em vez de substituir, o comportamento do pai com super.method(...):
class Logger {
void log(String s) { System.out.println(s); }
}
class TimestampedLogger extends Logger {
@Override
void log(String s) {
super.log("[" + System.currentTimeMillis() + "] " + s);
}
}A sobrescrita decora o comportamento do pai em vez de duplicá-lo. Abordado no capítulo sobre a palavra-chave super.
Um exemplo completo
O que vem a seguir
Você já viu como subclasses substituem métodos do pai. A próxima ideia — abstração — é o lado inverso: declarar um método que não tem nenhum corpo padrão e forçar cada subclasse concreta a fornecer um. Continue em abstração em Java.