No meu treinamento de Java básico existe um tópico denominado ‘recursos avançados’, onde eu ensino polimorfismo. Este tópico em especial tem sido uma experiência muito interessante, uma vez que eu venho aprimorando vários diferentes modelos de explicação e exercícios baseados no próprio feedback dos alunos.
Na verdade, eu tenho buscado, durante estes anos, aprimorar uma melhor forma de apresentar o conteúdo com o objetivo de fazer com que o aluno concretize efetivamente tanto a sintaxe como, principalmente, a semântica deste poderoso recurso da orientação a objetos.
E é justamente isso que quero expor neste artigo, de forma prática, rápida e conceitual, apresentando todos os detalhes relativamente importantes envolvidos dentro deste contexto.
Polimorfismo é o recurso que habilita um objeto a se comportar de maneiras variáveis ao longo de sua existência, dentro do programa orientado a objetos. O termo polimorfismo é originário do grego e significa “muitas formas”.
Tecnicamente falando, o polimorfismo é o nome dado para a implementação que permite que referências de tipos de superclasses mais abstratas apontem concretamente para instâncias de objetos de classes-filhas. Assim, é possível tratar várias hierarquias de tipos de maneira homogênea. Os benéficos que justificam o seu uso são clareza, poder de manutenção e flexibilidade.
Veremos aqui um exercício 100% prático. Vamos criar um contexto adicionado outros fatores e elementos que estarão gradualmente gerando o conhecimento. É assim que eu espero que aconteça com você, leitor.
Vamos imaginar que somos responsáveis por implementar um sistema que controla a fabricação e a venda de um simples restaurante de pizza de calabresa. Dado o contexto, poderíamos, então, escrever um projeto bem modesto de automatização.
Versão 1



A classe PizzaCalabreza implementa os comportamentos necessários para podermos fabricar, assar e cobrar o determinado valor pela pizza naquele restaurante. A classe Forno assume a responsabilidade de fabricar fisicamente a pizza, executando os passos corretos que prepara, assa e cobra a pizza. A classe Programa simplesmente é usada para possuir o método padrão que inicia todo programa desktop JSE.
Execute o programa e veja a seguinte saída ser apresentada no console:
molho, queijo, calabreza, cebola e tomate
15 minutos
R$ 12,00
Vamos imaginar que o programa está funcionando com sucesso, até que o proprietário do restaurante decide aumentar o cardápio e acrescenta um novo sabor de pizza. Consequentemente, fomos requisitados para alterar o programa que suporta o controle, agora de pizza napolitana.
Versão 2
Baseado nos novos requisitos, facilmente adicionamos uma nova classe no projeto chamada de PizzaNapolitana, responsável por implementar os detalhes do próprio sabor em particular.

E, consequentemente, alteramos o programa para executar a pizza com o novo sabor e deixar tudo funcionando, agora com a nova pizza.

Ao tentar executar novamente o sistema, notaremos um erro de compilação, no momento de fabricar a pizza de napolitana. Veja que existe um pedaço da linha grifado em vermelho, dizendo que existe um erro naquele lugar.
É muito comum esbarrarmos em diversos problemas na manutenção, quando temos que identificar a sua natureza e tomar decisões sobre eles. Eu costumo dizer que, para se tomar a decisão correta em sistemas OO, temos que visualizar as coisas em duas diferentes perspectivas:
- Técnica: consiste em levantar o fato em aspectos relacionados com a sintaxe de programação.
- Conceitual: consiste em levantar o fato em aspectos relacionados com a abstração da modelagem que está sendo automatizada.
Aplicando estas duas visões no nosso exemplo, teríamos algo assim:
- Técnica: O erro de compilação está acontecendo porque o método fabricar da classe Forno somente recebe objetos da classe PizzaCalabresa. E, neste caso, estamos passando um objeto errado.
- Conceitual: Existe um erro neste projeto OO porque construímos um forno que somente assa pizzas de calabresa e, agora, queremos colocar dentro deste forno uma pizza de um sabor que ele não foi construído para assar.
É importante pontuar que precisamos entender a parte conceitual do que estamos implementando, porque é ela que vai nos dar direção de como e onde intervir no projeto.
No caso do nosso exemplo, está claro que precisamos de um forno que tenha a capacidade de assar qualquer sabor de pizza, e que isso é como acontece no mundo real. Sem mais demoras, iremos mudar a classe Forno, capacitando-a a assar esse novo sabor de pizza. O passo mais lógico é aqui fazer uma sobrecarga de método, acrescentando outro que fabrica o sabor da nova pizza proposta.

Depois disso, já podemos voltar a executar, pois o sistema voltará a funcionar, agora fabricando os dois sabores de pizza. Execute o programa novamente e veja a seguinte saída ser apresentada no console:
molho, queijo, calabreza, cebola e tomate
15 minutos
R$ 12,00
molho, pressunto, queijo, tomate e oregano
19 minutos
R$ 18,00
Projeto de Classes
Este é o ponto tão esperado, onde sempre fazemos uma análise de todo o código escrito até aqui, verificando para qual direção o projeto está se deslocando. No exemplo aqui proposto, poderíamos afirmar que o projeto está no mesmo caminho do famoso navio Titanic. Ou seja, no fundo do mar da inflexibilidade!
Vejamos porque:
- Duplicação de Código: veja que copiamos e colamos o mesmo método de fabricação da pizza de calabresa para a napolitana, apenas mudando o tipo do parâmetro. Fizemos isso porque precisávamos fazer a classe Forno fabricar a outra pizza.
- Falta de Clareza: veja que implementamos métodos com nomes diferentes nas classes de pizzas, que correspondem ao mesmo comportamento de ambas. E, nos dois métodos de fabricar da classe Forno, acabamos tendo que corrigir de acordo.
- Baixa Manutenção: veja que, quando for solicitado para se adicionar outro sabor de pizza, teremos que repetir os passos de duplicação de código para fazer o forno assar o novo sabor. Neste caminho, todas as vezes que acrescentarmos uma nova pizza, o Forno terá que ser alterado para suportar a nova estrutura. Imagine agora qual será o tamanho da classe Forno quando tivermos uns 40 sabores de pizza? Sim, conterá 40 diferentes métodos chamados fabricar, que serão responsáveis por assar cada pizza. Se isso não fosse suficiente, imagine agora que, depois destes 40 tortuosos sabores implementados, testados e funcionando, nos tivéssemos que alterar o modelo de fabricação de pizza de todas as pizzas do sistema? Um simples exemplo disso seria se o proprietário nos requisitasse para acrescentar uma determinada quantidade de borda recheada com catupiri, e cada um dos sabores teria uma quantidade especifica. Ou seja, precisaríamos alterar os 40 métodos para todas as pizzas.
Este é um típico cenário onde o polimorfismo pode ser aplicado para corrigir estes problemas. Não podemos nos esquecer justamente dos benefícios do polimorfismo, que são clareza, poder de manutenção e flexibilidade, o oposto dos problemas encontrados.
A questão chave é que os responsáveis pelo sistema é que devem estar constantemente atentos para identificar os cenários e prontos para tomar o posicionamento correto na infraestrutura de classes.
Chegado ao ápice do nosso exercício, vamos evoluir o projeto, aplicando este tal de polimorfismo.
Versão 3
O primeiro passo é definir uma hierarquia “paternal”, com o intuito de gerar comportamento genérico para todas as supostas pizzas do projeto. No exemplo, decidi usar uma interface ao invés de uma classe abstrata ou concreta, que também poderia ser utilizada.

Nesta interface foram definidos os três métodos necessários para todas as pizzas do projeto. Os próximos passos são criar as pizzas herdando desta interface, subrescrevendo os métodos pendentes e definindo neles os comportamentos específicos de cada pizza em questão.

O próximo passo é considerado o mais importante. É nele que o “plin-plin” do polimorfismo acontece. Iremos alterar a classe Forno para se comportar polimorficamente, dando a ela a capacidade de assar qualquer tipo de pizza.

É muito importante entendermos que o método fabricar da classe Forno usa uma referência polimórfica, que é justamente o mecanismo chave de execução. A referência polimórfica pode apontar para qualquer objeto que seja filho da interface Pizza . Ou seja, trocando a explicação, isso quer dizer que este método tem a capacidade de receber parâmetros variáveis para todos os objetos de classes que sejam obrigatoriamente classes filhas da interface Pizza.
Para finalizar, alteramos a classe do programa, agora executando o nosso suposto exemplo.

Execute o programa pela última vez e veja que a saída apresentada é a mesma da versão 2. Ou seja, o projeto continuou fazendo a mesma coisa, porém estamos com um projeto OOP flexível. Fazendo uma análise final, vejamos os pontos anteriormente analisados:
- Duplicação de Código: não houve duplicação de código porque o único método da classe Forno é capaz de receber e executar polimorficamente qualquer outro objeto filho de Pizza.
- Falta de Clareza: não houve falta de clareza porque a interface pizza estabeleceu uma padronização nos nomes dos métodos herdados e corretamente sobrescritos nas classes pizzas filhas.
- Baixa Manutenção: veja que quando for solicitado para se adicionar outros sabores de pizzas, não será necessário alterar nada da classe Forno. Simplesmente teremos que fazer que a nova classe seja filha da interface Pizza e sobrepor os comportamentos em especifico daquele determinado novo sabor. O melhor de tudo seria se fôssemos requisitados para alterar o modelo de fabricação de pizza, como no exemplo anteriormente citado. Neste novo projeto, qualquer alteração no método fabricar irá propagar a alteração para todos os sabores de pizza no projeto! Ou seja, mexeu uma vez, alterou simplesmente tudo!
Conclusão
Eu costumo dizer nos meus cursos que OOP é a implementação da visão do mundo real em que vivemos. E, neste caso, o polimorfismo é o recurso utilizado para implementar os relacionamentos genéricos e variáveis que determinados objetos precisam ter com outros objetos.
Veja o caso do nosso exemplo, o forno é um objeto que precisa assar N tipos de diferentes objetos pizzas. Com isso, usamos o recurso do polimorfismo para expressar sintaticamente na linguagem de programação este tipo de relacionamento. O caso vai se repetir inúmeras vezes dentro de outros contextos, com outros escopos de objetos.
Todas as especificações, tecnologias, componentes e frameworks encontrados no mercado do mundo Java são construídos e elaborados usando, em alguma parte, o esquema de polimorfismo. Isso acontece porque eles necessitam ter, em alguma de suas partes, lacunas a serem preenchidas pela própria aplicação, justamente por serem usados em uma variação de diferentes de soluções.
Resumindo, dominar este conceito vai ter ajudar a entender e trabalhar com as potências do mundo Java.
Por hoje eu termino aqui, meus amigos, o artigo fica aberto para comentários gerais ou quaisquer outras dúvidas. Aquele grande abraço e até a próxima.







