Pesquisar neste blog

quinta-feira, 28 de junho de 2012

O que é Polimorfismo em Java?

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 algumas confusões. 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"? Tanto faz como "a" recebeu a referência a "b", 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
  }
}
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 é: 
  • 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.


Fonte: Polimorfismo

Nenhum comentário: