O documento explica o que é polimorfismo em Java. Resumidamente:
1) Polimorfismo ocorre em chamadas de métodos de objetos, permitindo que o método seja executado de diferentes formas dependendo do tipo do objeto.
2) Em Java, qualquer chamada de método de objeto é polimórfica, pois o objeto receptor decide como implementar o método.
3) Tipos abstratos como interfaces permitem polimorfismo, pois classes podem implementá-los de formas diferentes, mas herança também permite polimorfismo ao substituir
1. O que é Polimorfismo?
Jacques Philippe Sauvé
Muitos livros de programação falam de polimorfismo de forma equivocada. Até a famosa Monica
Pawlan pisou na bola, em minha opinião. Ela dá a impressão que o polimorfismo ocorre quando
passamos objetos como parâmetros numa chamada de método, o que não é verdade. Ela também dá
a impressão que o polimorfismo deve envolver uma superclasse, o que também não é verdade.
Outros autores famosos cometem os mesmos erros (Os autores de Core Java, por exemplo).
Então vamos tentar enxergar o que é polimorfismo. Na realidade, é bastante simples,
conceitualmente, embora os detalhes tenham um jeito de obscurecer a situação.
De forma genérica, polimorfismo significa "várias formas". Numa linguagem de programação, isso
significa que pode haver várias formas de fazer uma "certa coisa". Aí vem a primeira coisa
importante: que "certa coisa" é essa? A resposta é que estamos falando de chamadas de métodos.
Portanto, em Java, o polimorfismo se manifesta apenas em chamadas de métodos. Agora, podemos
ser mais específicos sobre a definição de polimorfismo: Polimorfismo significa que uma chamada
de método pode ser executada de várias formas (ou polimorficamente). Quem decide "a forma" é o
objeto que recebe a chamada. Essa última frase é muito importante, pois ela encerra a essência do
polimorfismo. Leia a frase novamente.
Ela significa o seguinte: Se um objeto "a" chama um método xpto() de um objeto "b", então o
objeto "b" decide a forma de implementação do método. Mais especificamente ainda, é o tipo do
objeto "b" que importa. Para concretizar melhor, digamos que xpto() seja grita(). Então a chamada
b.grita() vai ser um grito humano se "b" for um humano e será um grito de macaco, se o objeto "b"
for um macaco. O que importa, portanto, é o tipo do objeto receptor "b".
Podemos agora resolver uma das confusões de Monica Pawlan. O objeto "a" possui uma referência
para o objeto "b", obviamente, já que ele está chamando o método grita() do objeto "b". Isto é, ele
executa b.grita(). De onde veio essa referência ao objeto "b"? Monica Pawlan diz que ela foi
recebida como parâmetro pelo objeto "a" em alguma chamada de método. Não tem nada a ver.
Tanto faz como "a" recebeu a referência a "b". Pode ter sido como Pawlan falou ou pode ser de
várias outras formas. Vamos dar alguns exemplos:
1. O objeto "a" cria o objeto "b"
class A {
void facaAlgo() {
Gritador b;
if(...) {
b = new Humano();
} else {
b = new Macaco();
}
b.grita(); // chamada polimórfica
}
}
2. O objeto "a" recebe o objeto "b" de um objeto "c"
class A {
void facaAlgo() {
Gritador b = c.meDeUmGritador(); // "c" é um objeto qualquer para o qual tenho
referência
b.grita(); // chamada polimórfica
2. }
}
3. O objeto "a" recebe o objeto "b" numa chamada de método
class A {
void facaAlgo(Gritador b) {
b.grita(); // chamada polimórfica
}
}
O que Monica Pawlan falou é a forma 3. Tem outras formas ainda de obter essa referência. O
importante é que "a" tem uma referência a "b" e pronto. Não importa de onde ela vem.
Então onde ocorre o polimorfismo na linguagem Java? Resposta: nas chamadas de métodos. Agora
podemos perguntar: há polimorfismo nas chamadas de quais métodos? Resposta: Em
Java, todas as chamadas de métodos a objetos são polimórficas. Se você observar bem a última
frase, você vai observar duas coisas:
1. Estou falando de Java. Em algumas outras linguagens, como C++, você pode especificar
quais métodos são polimórficos e quais não são.
2. Voltando a Java, estou falando de "métodos de objetos". Isso significa que, em Java, não há
polimorfismo ao chamar métodos estáticos (também chamados de "métodos de classes").
Porém, métodos de objetos sempre são polimórficos.
Tudo que tem acima deve ser mais ou menos simples. A complicação começa agora. Não é tão
complicado assim, mas é o suficiente
para ter atrapalhado muitos autores de livros. Temos que falar de tipos.
Não é qualquer objeto que pode gritar, certo? Se eu tiver um objeto "b" representando uma cadeira
e fizer b.grita(), não pode sair coisa boa, porque uma cadeira não grita. Temos, portanto que
indicar, de alguma forma, o tipo de objeto que pode ser usado neste lugar. Fazemos isso usando
tipos. Você viu, acima, que a referência "b" é do tipo "Gritador". O que é Gritador? Na realidade,
para o compilador Java, não importa o que seja Gritador, desde que este tipo saiba gritar, isto é,
Gritador é qualquer coisa que tenha um método grita().
Aí está à confusão da maioria dos livros de programação O-O. Eles dizem que Gritador é uma
superclasse e é isso que causa o polimorfismo. Não é verdade. Há duas formas básicas de criar um
tipo em Java e ambas as formas podem ser usadas para definir Gritador. Uma forma de definir um
tipo (a mais correta, para mim) é assim:
interface Gritador {
void grita();
}
Esse é um tipo chamado "tipo abstrato" porque só dizemos que existe um método grita() sem dizer
nada sobre sua implementação. Isto é, como gritar não foi especificado. Agora, posso fazer com
que qualquer classe implemente este tipo. Veja abaixo:
class Humano implements Gritador {
public void grita() {
System.out.println("AAAAAAHHHHHHHAAAAHHHHHHAAAAAHHHHHA"); // Me Tarzan!
}
}
class Macaco implements Gritador {
public void grita() {
3. System.out.println("IIIIIIIIHHHHHHHIIIIHHHHHHIIIIIIHHHHHI"); // Me Cheetah!
}
}
As duas classes implementam o tipo Gritador e as chamadas polimórficas que mostrei mais acima
funcionarão sem problemas. Observe que não há superclasse envolvida! Não é necessário ter uma
hierarquia de classes para ter polimorfismo, embora quase todos os autores de livros apresentem
polimorfismo usando hierarquias de classes. O importante é: qualquer objeto que implementa o
tipo Gritador poderá ser usado nos exemplos que mostrei acima onde o objeto "a" quer tratar com
um gritador. Se, amanhã, eu criar uma nova classe:
class Aluno implements Gritador {
public void grita() {
System.out.println("naoquerofazerprovanaoquerofazerprovanaoquerofazerprova"); // Me
Joãozinho!
}
}
Então objetos dessa classe funcionarão nos exemplos anteriores na chamada b.grita().
Observe que, se eu tiver um programa com objetos das classes Humano, Macaco e Aluno, terei 3
implementações diferentes do método grita(). A chamada b.grita() está chamando um desses três
métodos, dependendo da classe do objeto "b". Achar o método correto a ser chamado para um
objeto particular chama-se dynamic binding, ou amarração dinâmica. Isto é, temos que amarrar a
chamada b.grita() a uma das implementações de grita() dinamicamente, em tempo de execução (e
não em tempo de compilação, o que se chamaria static binding).
Agora, vamos logo para a confusão. A herança também permite fazer polimorfismo porque a
herança permite criar várias classes que implementam o mesmo tipo. Lembre que se eu tiver várias
classes implementando o mesmo tipo, posso fazer polimorfismo (várias classes implementando
Gritador, por exemplo). Agora, ao definir uma classe:
class UmGritador {
public grita() {
System.out.println("Buuuuu");
}
}
eu também estou criando um tipo. Só que desta vez, ele não é abstrato. O tipo UmGritador é um
tipo concreto porque ele fornece uma implementação concreta do método grita(). Porém,
UmGritador não deixa de ser um tipo (ele é um tipo e a implementação deste tipo). Sendo assim, o
que ocorre quando uso herança? Veja:
class Humano extends UmGritador {
public void grita() {
System.out.println("AAAAAAHHHHHHHAAAAHHHHHHAAAAAHHHHHA");
}
}
Olhe o "extends" acima: estou fazendo herança. Ao fazer herança, objetos da classe Humano vão
herdar todos os métodos da superclasse UmGritador. Isto significa que, com herança, vou herdar o
tipo da superclasse e também a implementação da superclasse. O polimorfismo vem agora. Preste
atenção. Ao herdar, a subclasse pode fazer override (substituir) alguns métodos. É isso que
Humano fez, acima: ele decidiu gritar de forma diferente. Isso significa que objetos da classe
UmGritador, ou da classe Humano ou da classe Macaco terão formas diferentes de implementar
gritar(). Portanto, haverá polimorfismo ao chamara b.gritar()
4. Vou pegar um exemplo anterior e alterar só o tipo na definição do objeto "b":
class A {
void facaAlgo() {
UmGritador b;
if(...) {
b = new Humano();
} else {
b = new Macaco();
}
b.grita(); // chamada polimórfica
}
}
Agora, o tipo é uma superclasse e não um tipo abstrato (interface). Terei polimorfismo, sim, porque
tenho vários objetos que implementam o mesmo tipo e que possuem implementações diferentes do
método gritar(). Posso fazer isso com herança ou posso fazer isso sem herança. A confusão de
muitos autores de livros é que eles apresentam polimorfismo com herança, dando a impressão
que tem que ter herança para ter polimorfismo.
Eu prefiro apresentar polimorfismo com tipos abstratos (interface, em Java), para deixar claro que
polimorfismo é uma coisa, herança é outra (embora haja ligação).
Espero que tudo isso não esteja te deixando mais confuso!
Agora, vamos terminar a discussão dizendo: para fazer polimorfismo, é melhor usar tipos abstratos
ou tipos concretos (herança)? Nem todo mundo concorda com a melhor forma de fazer isso. Minha
opinião é:
Use tipos abstratos (interface) para fazer polimorfismo
Use herança para fatorar código comum entre várias classes
Em outras palavras:
Defina comportamentos ("ser um gritador") com tipos abstratos (interfaces) e use-os no
polimorfismo
Defina implementações (como gritar) com classes e use superclasses para fatorar
implementações comuns.
Espero ter ajudado.
Jacques