1. Polimorfismo em coleções: para que serve?
Vamos começar com uma pergunta: “Afinal, para que serve o polimorfismo em programas reais?”
Imagine um zoológico. Você tem uma classe base Animal e um monte de subclasses: Dog, Cat, Cow, Parrot e até Platypus (ornitorrinco, para os amantes de exotismo). Cada um deles sabe emitir um som (makeSound()), mas faz isso à sua maneira.
Em vez de criar arrays separados para cada animal, você declara um array ou lista do tipo Animal e coloca ali quem quiser:
Animal[] animals = {
new Dog(),
new Cat(),
new Cow(),
new Parrot()
};
Agora você pode percorrer esse array e chamar makeSound() para cada um:
for (Animal animal : animals) {
animal.makeSound();
}
Magia! Cada objeto sabe qual som emitir, e você não precisa escrever um monte de if ou switch.
Exemplo com analogia
É como se você desse o comando “Voz!” a um grupo de animais, e cada um decidisse sozinho o que fazer: o cachorro late, o gato mia e a vaca — muge. Você não especifica quem é quem — apenas chama o mesmo método.
2. Exemplo prático: hierarquia de funcionários
Vamos fazer um exemplo mais próximo da vida real (e do futuro trabalho em TI). Imagine que temos uma empresa com diferentes funcionários: gerentes, desenvolvedores, testadores. Cada um tem o método work(), mas ele é executado de forma diferente.
Declaração da classe base
public class Employee {
public void work() {
System.out.println("O funcionário está trabalhando...");
}
}
Subclasses
public class Manager extends Employee {
@Override
public void work() {
System.out.println("O gerente está conduzindo uma reunião.");
}
}
public class Developer extends Employee {
@Override
public void work() {
System.out.println("O desenvolvedor está escrevendo código.");
}
}
public class Tester extends Employee {
@Override
public void work() {
System.out.println("O testador está procurando bugs.");
}
}
Usando um array/lista do tipo base
public class CompanyDemo {
public static void main(String[] args) {
Employee[] team = {
new Manager(),
new Developer(),
new Tester(),
new Developer()
};
for (Employee e : team) {
e.work(); // Será chamada a versão "correta" do método para cada objeto
}
}
}
Resultado da execução:
O gerente está conduzindo uma reunião.
O desenvolvedor está escrevendo código.
O testador está procurando bugs.
O desenvolvedor está escrevendo código.
Quais são as vantagens?
- Você não escreve um monte de verificações do tipo “Se for Developer — faça isso”.
- Adicionar um novo funcionário (por exemplo, Designer) — basta criar uma classe nova e adicioná-lo ao array.
- O código que usa o array de funcionários não muda em nada!
3. Vantagens do polimorfismo: flexibilidade e extensibilidade
Vamos imaginar que surgiu um novo tipo de funcionário na sua empresa — Designer. Tudo o que você precisa fazer é criar uma nova classe:
public class Designer extends Employee {
@Override
public void work() {
System.out.println("O designer está criando layouts.");
}
}
Agora você pode adicionar o designer à equipe:
Employee[] team = {
new Manager(),
new Developer(),
new Tester(),
new Designer()
};
Voilà! O programa passa a funcionar corretamente com o novo tipo de funcionário, sem mudar uma única linha no código que itera o array e chama work().
Isso é a extensibilidade: seu código se adapta facilmente a novos tipos de objetos.
4. Limitações do polimorfismo: o outro lado da moeda
Infelizmente, toda “magia” tem suas limitações (e um custo, como em qualquer RPG).
Apenas os métodos da classe base ficam disponíveis
Quando você trabalha com uma variável do tipo Employee, pode chamar apenas os métodos declarados na classe Employee. Se a classe Developer tiver um método específico writeCode(), você não conseguirá chamá-lo diretamente:
Employee e = new Developer();
// e.writeCode(); // Erro de compilação: esse método não existe em Employee!
Se você realmente quiser chamar um método específico, terá que fazer casting. Mas isso é um último recurso. Se você faz casts com frequência, talvez deva repensar o design das classes — a classe base ou a interface deveria conter o método necessário.
if (e instanceof Developer) {
Developer dev = (Developer) e;
dev.writeCode();
}
Mas aí você perde a universalidade e a elegância pela qual tudo isso foi pensado. Portanto, procure projetar a classe base de forma que ela tenha apenas os métodos que realmente são necessários a todos os herdeiros.
5. Prática: implementando a hierarquia de funcionários
Vamos unir o útil ao agradável: vamos escrever um aplicativo simples, com alguns tipos de funcionários, e usar polimorfismo para tratá-los.
Passo 1: classe base e subclasses
// Employee.java
public class Employee {
public void work() {
System.out.println("O funcionário está trabalhando...");
}
}
// Manager.java
public class Manager extends Employee {
@Override
public void work() {
System.out.println("O gerente está conduzindo uma reunião.");
}
}
// Developer.java
public class Developer extends Employee {
@Override
public void work() {
System.out.println("O desenvolvedor está escrevendo código.");
}
}
// Tester.java
public class Tester extends Employee {
@Override
public void work() {
System.out.println("O testador está procurando bugs.");
}
}
Passo 2: classe principal
// CompanyDemo.java
public class CompanyDemo {
public static void main(String[] args) {
Employee[] team = {
new Manager(),
new Developer(),
new Tester(),
new Developer()
};
for (Employee e : team) {
e.work();
}
}
}
Passo 3: adicionando extensibilidade
Suponha que, em um mês, apareça um novo funcionário — um designer. Tudo o que é preciso:
public class Designer extends Employee {
@Override
public void work() {
System.out.println("O designer está criando layouts.");
}
}
Pronto, agora você pode adicionar o designer à equipe:
Employee[] team = {
new Manager(),
new Developer(),
new Tester(),
new Designer()
};
Conclusão
Todo o código principal (CompanyDemo) permaneceu inalterado! Essa é a força do polimorfismo.
6. Erros comuns ao usar polimorfismo
Erro nº 1: Esperar acesso a métodos específicos por meio de uma referência do tipo base.
Muito frequentemente, iniciantes tentam chamar métodos específicos da subclasse por meio de uma variável do tipo da superclasse. Por exemplo:
Employee e = new Developer();
// e.writeCode(); // Erro! Esse método não está definido em Employee.
Para chamar um método específico, é preciso fazer casting, mas aí perde-se a universalidade.
Erro nº 2: Não usar a anotação @Override.
Se você esquecer a anotação, pode acidentalmente escrever um método novo em vez de sobrescrever (por exemplo, errar no nome). Nesse caso, o polimorfismo simplesmente não funcionará, e será chamada a versão da superclasse.
Erro nº 3: Ausência de uma interface comum.
Se a classe base não contiver o método necessário, o polimorfismo é impossível. Por exemplo, se em Employee não houver o método work(), o laço sobre o array de funcionários não conseguirá chamar esse método para todos.
Erro nº 4: Violação do princípio aberto/fechado.
Se, para adicionar um novo tipo de funcionário, for preciso mudar o código que percorre o array/lista, então você não está usando o polimorfismo corretamente.
GO TO FULL VERSION