O que é Polimorfismo?

Jacques Philippe Sauvé

Iniciamos com uma pergunta de Francisco Fabian M Almeida:

Professor,
Certas questões às vezes deixam as pessoas em meio a um dilema.  Isto aconteceu comigo quando se perguntou: o que eh polimorfismo?
Ou melhor: quando ocorre polimorfismo? A resposta para essa pergunta poderia ser dita de uma forma bem simples  (da maneira que a prof. Livia ensinou: me lembro do falar dos cachorros e dos seres humanos).  Mas o que faz o dilema tornar-se veridico eh que em algumas publicações os autores  abordam o aspecto do polimorfismo diferentemente do habitual.  O que mais me deixa com uma interrogação eh que ninguém comenta nada sobre o fato.  Para vc ter uma ideia do que estou falando,  eu copiei um trecho de um livro eletronico chamado  Essentials of the JavaTM Programming Language: A Hands-On Guide, Part 2. Como vc verá, ele trata o polimorfismo de uma forma diferente da habitual e  constitui assim o dilema:  O que eh polimorfismo? (Mais precisamente: Onde ocorre polimorfismo na linguagem Java?).

/////////////////////////////////////////////////////////////////////////////////
FONTE: Essentials of the JavaTM ProgrammingLanguage: A Hands-On Guide, Part 2 by Monica Pawlan
/////////////////////////////////////////////////////////////////////////////////
Polymorphism
Another way objects work together is to define methods that takeother objects as parameters.  You get even more cooperation and efficiency when the objects are united by a common superclass. All classes in the Java programming language have an inheritance relationship. For example, if you define a method that takes a java.lang.Object as a parameter,  it can accept any object in the entire Java platform.  If you define a method that takes a java.awt.Component as a parameter, it can accept any component object.  This form of cooperation iscalled polymorphism. You saw an example of polymorphism in Part 2, Lesson 5:Collections  where a collection object can contain any type of objectas long as it descends from java.lang.Object. It is repeated here toshow you that Set collection can add a String object and an Integer object to the Set because the Set.add method is defined to accept any class instance that traces back to the java.lang.Object class.

String custID = "munchkin";
Integer creditCard = new Integer(25);
Set s = new HashSet();
s.add(custID);
s.add(creditCard);

/////////////////////////////////////////////////////////////////////////////////

Resposta

Francisco,
Sua pergunta é muito boa e, aparentemente, o assunto não está claro para muita gente, haja vista que muitos livros de programação falam de polimorfismo de forma equivocada. Até a famosa Monica Pawlan pisou na bola, na 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 autrores 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 façaAlgo() {
    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 façaAlgo() {
    Gritador b = c.meDêUmGritador(); // "c" é um objeto qualquer para o qual tenho referência
    b.grita(); // chamada polimórfica
  }
}

3. O objeto "a" recebe o objeto "b" numa chamada de método

class A {
  void façaAlgo(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á a 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() {
    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 podemos 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()

Vou pegar um exemplo anterior e alterar só o tipo na definição do objeto "b":

class A {
  void façaAlgo() {
    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 é: 

Em outras palavras:

Espero ter ajudado.

Jacques