1. Sobrescrita de métodos
Sobrescrita (overriding) — é a possibilidade de escrever, em uma subclasse, sua própria versão de um método que já existe na classe pai. Graças a isso, funciona o polimorfismo real em tempo de execução (run-time).
Em palavras simples:
Se você tem uma classe base com um método e quer que a subclasse execute esse método do seu próprio jeito, basta declarar um método com a mesma assinatura na subclasse. Ao chamar o método por meio de uma referência do tipo base, será chamada a versão do método do tipo real do objeto.
Exemplo do dia a dia.
Imagine que você tem um grupo de animais e pede a cada um “emitir um som”. Para todos, o comando é makeSound(), mas o cachorro late, o gato mia e a vaca muge. No código, isso parece a chamada do mesmo método, mas o resultado é diferente — eis a magia da sobrescrita!
Sintaxe da sobrescrita
Exemplo básico
class Animal {
void makeSound() {
System.out.println("O animal emite um som");
}
}
class Dog extends Animal {
@Override
void makeSound() {
System.out.println("Au!");
}
}
class Cat extends Animal {
@Override
void makeSound() {
System.out.println("Miau!");
}
}
Aqui, na classe Animal, está definido o método makeSound(). As subclasses Dog e Cat sobrescrevem esse método, fornecendo sua própria implementação.
Anotação @Override
Em Java, é recomendado (e é um bom hábito) marcar métodos sobrescritos com a anotação @Override:
@Override
void makeSound() { ... }
Isso não é obrigatório para o código funcionar, mas:
- O compilador verificará se você realmente está sobrescrevendo um método (e não digitou errado o nome ou os parâmetros por engano).
- Melhora a legibilidade do código — outros programadores veem imediatamente que esse método sobrescreve o do pai.
Chamada de um método sobrescrito
Animal myDog = new Dog();
myDog.makeSound(); // Irá imprimir: Au!
Embora a variável seja do tipo Animal, na prática ela aponta para um objeto Dog, e o método chamado é o da classe Dog. Isso é vinculação dinâmica (tardia).
2. Diferença entre sobrescrita (overriding) e sobrecarga (overloading)
Sobrecarga (overloading)
- Na mesma classe (ou dentro da hierarquia, mas sempre “perto”).
- Os métodos têm o mesmo nome, mas parâmetros diferentes (tipo, quantidade, ordem).
- A escolha do método ocorre em tempo de compilação.
void print(int x) { ... }
void print(String s) { ... }
Sobrescrita (overriding)
- Em classes diferentes: o método é declarado na superclasse e sobrescrito na subclasse.
- Os métodos têm o mesmo nome e assinatura (parâmetros e tipo de retorno).
- A escolha do método ocorre em tempo de execução (run-time), com base no tipo real do objeto.
Tabela comparativa
| Sobrecarga (overloading) | Sobrescrita (overriding) | |
|---|---|---|
| Onde | Na mesma classe | Na superclasse e na subclasse |
| Nome do método | Igual | Igual |
| Parâmetros | Diferentes | Iguais |
| Tipo de retorno | Pode ser diferente | Deve corresponder ou ser um subtipo |
| Quando é escolhido | Compilação | Execução (run-time) |
| Anotação | Não é necessário | @Override (recomendado) |
3. Regras de sobrescrita de métodos
Sobrescrever é algo poderoso, mas vem com regras rígidas. Vamos analisá-las em ordem.
A assinatura do método deve coincidir
- O nome do método, os tipos e a ordem dos parâmetros devem ser idênticos aos do método na superclasse.
- O tipo de retorno deve coincidir ou ser covariante (ou seja, um subtipo do tipo de retorno do método da superclasse).
Exemplo com tipo de retorno covariante:
class Animal {
Animal reproduce() { return new Animal(); }
}
class Cat extends Animal {
@Override
Cat reproduce() { return new Cat(); } // OK! Cat é um subtipo de Animal
}
Modificador de acesso
O modificador de acesso do método sobrescrito não pode ser mais restritivo do que o do método na superclasse. Se o método no pai é public, então na subclasse ele deve continuar public. Não é permitido torná-lo menos acessível (protected ou private).
Exemplo:
class Parent {
public void greet() { }
}
class Child extends Parent {
// void greet() { } // Erro! O modificador padrão — package-private — é menos acessível do que public
@Override
public void greet() { } // OK
}
Exceções
- Um método sobrescrito não pode lançar novas exceções checked que não estejam declaradas no método base.
- Pode lançar menos ou as mesmas exceções.
Exemplo:
class Parent {
void doWork() throws IOException { }
}
class Child extends Parent {
@Override
void doWork() throws FileNotFoundException { } // OK, FileNotFoundException é um subtipo de IOException
// void doWork() throws SQLException { } // Erro! SQLException não está declarado no pai
}
Métodos estáticos não são sobrescritos
Métodos estáticos podem ser escondidos (hidden), mas não sobrescritos. Se você declarar um método estático com a mesma assinatura na subclasse, isso não é sobrescrita! Será apenas ocultação do método, e não polimorfismo.
class Animal {
static void info() { System.out.println("Animal"); }
}
class Dog extends Animal {
static void info() { System.out.println("Dog"); }
}
A chamada Dog.info() imprimirá "Dog", mas, se chamar por meio de uma variável do tipo Animal, será chamado o método Animal.info(). Isso não é polimorfismo!
final-métodos não podem ser sobrescritos
Se um método na superclasse for declarado como final, tentar sobrescrevê-lo resultará em erro de compilação.
class Animal {
final void sleep() { }
}
class Dog extends Animal {
// @Override
// void sleep() { } // Erro! Não é possível sobrescrever um método final
}
4. Exemplos práticos
Vamos ver na prática como a sobrescrita funciona e como ela se diferencia da sobrecarga.
Exemplo 1: classe Shape e suas subclasses
class Shape {
void draw() {
System.out.println("Desenhando uma figura");
}
}
class Circle extends Shape {
@Override
void draw() {
System.out.println("Desenhando um círculo");
}
}
class Rectangle extends Shape {
@Override
void draw() {
System.out.println("Desenhando um retângulo");
}
}
Usando polimorfismo:
public class Main {
public static void main(String[] args) {
Shape s1 = new Circle();
Shape s2 = new Rectangle();
s1.draw(); // Desenhando um círculo
s2.draw(); // Desenhando um retângulo
}
}
Embora as variáveis sejam declaradas como Shape, o método chamado é o da classe à qual o objeto realmente pertence.
Exemplo 2: Diferença em relação à sobrecarga
class Printer {
void print(String s) {
System.out.println("String: " + s);
}
void print(int n) {
System.out.println("Número: " + n);
}
}
Aqui, ambos os métodos se chamam print, mas têm parâmetros diferentes — isso é sobrecarga, não sobrescrita.
5. Sobrescrita e chamada do método da classe pai (super)
Às vezes, no método sobrescrito, você quer primeiro executar a lógica do pai e depois adicionar a sua. Para isso, usa-se a palavra-chave super.
class Animal {
void makeSound() {
System.out.println("O animal emite um som");
}
}
class Dog extends Animal {
@Override
void makeSound() {
super.makeSound(); // Chamada do método da classe pai
System.out.println("Au!");
}
}
A chamada new Dog().makeSound() imprimirá:
O animal emite um som
Au!
Como funciona a vinculação dinâmica (late binding)
Quando você chama um método por meio de uma referência do tipo base, o Java, em tempo de execução, verifica a que objeto real essa referência aponta e chama exatamente a versão do método que está definida na classe desse objeto.
Animal a = new Cat();
a.makeSound(); // Chamará Cat.makeSound(), e não Animal.makeSound()
Essa é a base do polimorfismo em Java.
6. Como isso se relaciona ao seu aplicativo
No nosso aplicativo de aprendizado (por exemplo, um sistema de gestão de funcionários), você pode criar a classe base Employee com o método work(), e as subclasses Manager e Developer podem implementar esse método à sua maneira:
class Employee {
void work() {
System.out.println("O funcionário trabalha");
}
}
class Manager extends Employee {
@Override
void work() {
System.out.println("O gerente lidera");
}
}
class Developer extends Employee {
@Override
void work() {
System.out.println("O desenvolvedor escreve código");
}
}
Agora você pode armazenar todos os funcionários em um único array ou lista:
Employee[] employees = {new Manager(), new Developer(), new Developer()};
for (Employee e : employees) {
e.work(); // Para cada um — sua própria saída!
}
7. Erros comuns ao sobrescrever métodos
Erro nº 1: erro de digitação no nome do método ou nos parâmetros. Se você se enganou no nome do método ou em seus parâmetros, você não está sobrescrevendo o método, e sim criando um novo. Como resultado, o polimorfismo não funciona. Por isso, sempre use a anotação @Override — o compilador vai corrigir você na hora.
Erro nº 2: modificador de acesso mais restritivo. Se no pai o método é public e, na classe filha, você o declarar como protected ou sem modificador — ocorrerá um erro de compilação.
Erro nº 3: tentar sobrescrever um método static ou final. Métodos estáticos não são sobrescritos, e métodos final simplesmente não podem ser sobrescritos. Se você tentar — o compilador irá barrar.
Erro nº 4: alterar o tipo de retorno para um incompatível. Se o tipo de retorno do método na subclasse não coincide com o da superclasse (e não é um subtipo dele), o compilador não permitirá sobrescrever o método.
Erro nº 5: adicionar novas exceções checked. Um método sobrescrito não pode lançar novas exceções checked que não estejam na declaração do método base. Se fizer isso — o compilador emitirá um erro.
Erro nº 6: esquecer o super. Se, no método sobrescrito, você quiser preservar parte do comportamento do pai, não esqueça de chamar explicitamente super.methodName(). O Java não fará isso automaticamente.
Agora você sabe como a sobrescrita de métodos funciona, como ela difere da sobrecarga e como ela viabiliza o polimorfismo em Java. Na próxima aula, veremos como aplicar o polimorfismo na prática — com coleções, arrays e tarefas reais!
GO TO FULL VERSION