Polimorfismo
•
Apresentar
o polimorfismo e sua utilidade em desacoplar classes
•
Apresentar
a implementação de polimorfismo através de ligação dinâmica (dynamic ou
late binding)
• Devemos escrever um afinador capaz de afinar diferentes instrumentos
• Um afinador afina realizando uma série de operações, dentre elas, ele toca o instrumento
• Um possível código segue
package p2.exemplos; public abstract class
Instrumento { public enum
Nota { C, D,
E, F, G, A, B; //muitas
outras notas aqui! } public abstract void
toca(Nota n); //muitas
outras coisas!!!! } |
package p2.exemplos; public class
Violao extends Instrumento { @Override public void
toca(Nota n) { System.out.println("Violao.toca() " + n); } //muitas
outras coisas!!! } package p2.exemplos; public class Sax extends Instrumento { @Override public void
toca(Nota n) { System.out.println("Sax.toca() " + n); } //muitas
outras coisas!!! } package p2.exemplos; public class
Flauta extends Instrumento { @Override public void
toca(Nota n) { System.out.println("Flauta.toca() " + n); } //muitas
outras coisas!!! } package p2.exemplos; public class Baixo extends Instrumento { @Override public void
toca(Nota n) { System.out.println("Baixo.toca() " + n); } //muitas
outras coisas!!! } |
package p2.exemplos; import java.util.Scanner; import p2.exemplos.Instrumento.Nota; public class
AfinadorTipado { public static void afina(Flauta f) { //... f.toca(Nota.C); //... } public static void afina(Sax s)
{ //... s.toca(Nota.C); //... } public static void afina(Violao
v) { //... v.toca(Nota.C); //... } public static void
afina(Baixo b) { //... b.toca(Nota.C); //... } public static void main(String[]
args) { Scanner
sc = new Scanner(System.in); int escolha = 4; while (escolha <= 4 && escolha > 0) { prompt(); escolha = sc.nextInt(); switch (escolha) { case 1: Flauta flauta = new Flauta(); afina(flauta); break; case 2: Sax
sax = new Sax(); afina(sax); break; case 3: Baixo baixo = new Baixo(); afina(baixo); break; case 4: Violao violao = new Violao(); afina(violao); break; } } } private static void prompt() { System.out.println("Que instrumento voce quer afinar?"); System.out.println("1. Para afinar a flauta;"); System.out.println("2. Para afinar o sax;"); System.out.println("3. Para afinar o baixo;"); System.out.println("4. Para afinar o violao;"); System.out.println(">4 ou <0 Para sair."); } } |
• Vamos rodar o AfinadorTipado?
› Note que ele é capaz de afinar quaisquer dos tipos de instrumentos já existentes
• Qual o problema com este código?
› Não esgotamos ainda as possibilidades de instrumentos!
i) Novos instrumentos podem surgir
› Toda vez que um novo instrumento é adicionado, um novo método afina deve ser escrito;
› Se você esquecer de escrever o método afina(NovoInstrumento i) para o novo instrumento, não vai ocorrer erro de compilação, até que você tente afinar o novo instrumento
› E se um outro método, como por exemplo, testaAfinacao (TipoDeInstrumento i) tiver que ser criado na classe Afinador? Assim como afina(TipoDeInstrumento i), deve haver um “testaAfinacao” para cada tipo de instrumento
i) E se novos tipos de instrumento forem criados, então novos métodos “testaAfinacao” também devem ser criados
ii) Mas também tem os métodos “afina”…
› Note que gerenciar isso se torna muito complicado e sujeito a erros
• Qual a solução?
› Apenas um método afina (e apenas um método testaAfinacao, se este existir), que recebam como argumento uma objeto da super classe de todos os instrumentos (Instrumento) e não um objeto do tipo específico de instrumento
› É como se esquecêssemos que as classes derivadas existem ao criar a classe Afinador
› Na hora de afinar mesmo ou testar a afinação de um instrumento específico e o afinador precisar tocar o instrumento, então este instrumento específico vai ser passado como argumento para os métodos afina (e testaAfinacao)
i) Apesar do método estar esperando um Instrumento, isso não daria erro de compilação, pois trata-se de um upcasting
• Como seria essa solução em Java?
package p2.exemplos; import p2.exemplos.Instrumento.Nota; public class Afinador { public static void
afina(Instrumento i) { //... i.toca(Nota.C); //... } public static void main(String[]
args) { Instrumento
instrumento = null; Scanner
sc = new Scanner(System.in); int escolha = 4; while (escolha <= 4 && escolha > 0) { prompt(); escolha = sc.nextInt(); switch (escolha) { case 1: instrumento
= new Flauta(); break; case 2: instrumento
= new Sax(); break; case 3: instrumento
= new Baixo(); break; case 4: instrumento
= new Violao(); break; } if (instrumento != null) afina(instrumento); } } private static void
prompt() { System.out.println("Que instrumento voce quer afinar?"); System.out.println("1. Para afinar a flauta;"); System.out.println("2. Para afinar o sax;"); System.out.println("3. Para afinar o baixo;"); System.out.println("4. Para afinar o violao;"); System.out.println(">4 ou <0 Para sair."); } } |
|
•
Quando olhamos para o método afina acima, está
claro o que ele vai fazer? › A princípio, isto é, em tempo de compilação, não se sabe o que o método afina vai fazer L › O método afina espera uma referência a um instrumento, mas quando recebe uma referência a um instrumento que deriva de Instrumento, como saberá que instrumento é? › Como sabe que deve chamar o toca da flauta e não do violão? |
• O que é complicado nesse mundo? Qual a mágica?
› Como o compilador sabe que método chamar? Se ele vai chamar o de Flauta, ou o de Violao, ou o de outro instrumento qualquer…
› Esta mágica ocorre em Java (e em outras linguagens) devido ao momento em que a chamada a um método é associada ao corpo (implementação) do método
i) A esta atividade chamamos binding
› Em muitas linguagens isso é feito em tempo de compilação (early binding), então em tempo de compilação deve-se saber exatamente que método chamar
i) Isto não serviria para o nosso caso
ii) Veja o exemplo acima… não está claro olhando o código se iremos chamar o método afina de Flauta, de Violao, de Sax ou de Baixo
› A associação pode ser feita em tempo de execução (late binding)
i) É assim que ocorre em java
ii) A escolha é feita com base no tipo de objeto realmente passado como parâmetro
(1) Esperava-se receber um Instrumento, mas de fato, passou-se um objeto que deriva de Instrumento
(2) A associação é feita levando em consideração este objeto que foi passado em substituição à superclasse
(3) O objetos precisam de alguma forma saber responder sobre seu tipo, para que em tempo de execução o método correto seja chamado
(4) Em Java toda associação de chamada de método com corpo de método (binding) é feito em tempo de execução, exceto se o método for final ou static
› Uma vez que se sabe que objeto se tem na mão, então procura-se dentre os métodos definidos na classe deste objeto a implementação deste método
i) Lembre que o método em questão pode ser um método herdado e não sobrescrito, em cujo caso levará para a implementação da classe mãe mais próxima
(1) A busca vai subindo na hierarquia de classes até que o método seja encontrado em alguma superclasse, ou até que se chegue na raiz, em cujo caso ocorrerá um erro de compilação
(2) Assim, a redefinição de um método em uma subclasse esconde os métodos definidos nas superclasses das subclasses que esta classe possa vir a ter
Acepções |
|
• Diante
de tudo que vimos, o que é polimorfismo?
• A chamada i.toca(Nota.C) é polimórfica, pois faz coisas diferentes dependendo do objeto que a recebe
› Se for um sax, então tocará uma nota no sax, se for um violão, tocará uma nota do violão e assim sucessivamente
•
A mesma chamada a um método faz coisas diferentes
dependendo do objeto que recebe a mensagem
• Métodos de classe não permitem polimorfismo. Vejamos um exemplo.
package p2.exemplos; /** * Exemplo que demonstra que métodos estáticos não são polimórficos.<br> * (Exemplo derivado de "Thinking in Java") * @author Raquel Lopes * */ public class StaticSuper { public static String STATIC_STRING = "Base
Static String"; public static String staticGet() { return "Base staticGet()"; } public String dynamicGet() { return "Base dynamicGet()"; } } |
package p2.exemplos; /** * Exemplo que demonstra que métodos estáticos não são polimórficos.<br> * * @author Raquel Lopes * */ public class
StaticDerived1 extends StaticSuper { public static String STATIC_STRING = "Derived1
Static String"; public static String staticGet() { return "Derived1 staticGet()"; } @Override public String dynamicGet() { return "Derived1 dynamicGet()"; } } |
package p2.exemplos; /** * Exemplo que demonstra que métodos estáticos não são polimórficos.<br> * * @author Raquel Lopes * */ public class
StaticDerived2 extends StaticSuper { public static String STATIC_STRING = "Derived2
Static String"; public static String staticGet() { return "Derived2 staticGet()"; } @Override public String dynamicGet()
{ return "Derived2 dynamicGet()"; } } |
package p2.exemplos; /** * Exemplo que demonstra que métodos estáticos não são polimórficos.<br> * * @author Raquel Lopes * */ public class
StaticPolymorphism { /** * @param args */ public static void main(String[]
args) { StaticSuper
s1 = new StaticDerived1(); StaticSuper
s2 = new StaticDerived2(); StaticDerived1
d1 = new StaticDerived1(); StaticDerived2
d2 = new StaticDerived2(); printStaticObject(s1); printStaticObject(s2); printStaticObject(d1); printStaticObject(d2); } private static void
printStaticObject(StaticSuper s) { System.out.println(s.staticGet()); System.out.println(s.dynamicGet()); System.out.println(s.STATIC_STRING); System.out.println("______________________"); } private static void
printStaticObject(StaticDerived1 s) { System.out.println(s.staticGet()); System.out.println(s.dynamicGet()); System.out.println(s.STATIC_STRING); System.out.println("______________________"); } private static void
printStaticObject(StaticDerived2 s) { System.out.println(s.staticGet()); System.out.println(s.dynamicGet()); System.out.println(s.STATIC_STRING); System.out.println("______________________"); } } |
•
A saída deste programa é como segue:
Base
staticGet() Derived1
dynamicGet() Base
Static String ______________________ Base
staticGet() Derived2
dynamicGet() Base
Static String ______________________ Derived1
staticGet() Derived1
dynamicGet() Derived1
Static String ______________________ Derived2
staticGet() Derived2
dynamicGet() Derived2
Static String ______________________ |
• Note
que a chamada ao método static não é polimórfica. O método da classe mãe sempre
é chamado.
› Isso ocorre porque o binding de métodos static ocorre em tempo de compilação, quando você define o tipo de sua referência. Se sua referência é para um objeto StaticSuper, em tempo de compilação o compilador irá associar a chamada ao método staticGet de StaticSuper
› Mesmo que em tempo de execução a referência aponte para um objeto de uma classe derivada de StaticSuper, isso não é levado em consideração, pois o binding dos métodos de classe já foi realizado
• Veja que você não consegue usar @Override para um método estático
› Porque métodos estáticos não são sobrescritos
› Se você cria um método estático com mesmo nome, como ocorreu no exemplo, então todos existirão, e será chamado o método da classe que foi indicada ao definir a referência (não ao fazer new!)
• Herdamos tudo da classe base, mas só conseguimos ter acesso ao que é public ou protected
• Para garantir que o objeto da classe base que foi automaticamente inserido como um membro da classe derivada foi corretamente inicializado, então temos que chamar o construtor da classe base para iniciá-lo
• Quando temos muitos níveis de herança, em que ordem os construtores são chamados?
• Vamos criar um programa que demonstra esta ordem?
package p2.exemplos; public class Refeicao { public Refeicao() { System.out.println("Refeicao()"); } } |
package p2.exemplos; public class Almoco extends Refeicao { public Almoco() { System.out.println("Almoco()"); } } |
package p2.exemplos; public class AlmocoPortatil extends Almoco { public AlmocoPortatil() { System.out.println("AlmocoPortatil()"); } } |
package p2.exemplos; public class MistoQuente extends AlmocoPortatil { private Pao pao = new Pao(); private Queijo queijo = new Queijo(); private Presunto presunto = new Presunto(); public MistoQuente() { System.out.println("MistoQuente()"); } public static void main(String[] args) { new MistoQuente(); } } |
package p2.exemplos; public class Pao { public Pao() { System.out.println("Pao()"); } } |
package p2.exemplos; public class Presunto { public Presunto() { System.out.println("Presunto()"); } } |
package p2.exemplos; public class Queijo { public Queijo() { System.out.println("Queijo()"); } } |
• O
que vocês acham que acontecerá?
• Veja a saída:
Refeicao() Almoco() AlmocoPortatil() Pao() Queijo() Presunto() MistoQuente() |
• O
que aprendemos?
› No construtor do objeto sendo criado, há uma chamada (que pode ser escondida) ao construtor da super classe
i) E isso ocorre recursivamente até que chegue na raiz e o objeto da classe mãe raiz seja criado
ii) Então os objetos de todas as classes base são criados antes que o corpo do construtor seja executado
› Antes de executar o corpo do construtor os objetos inicializados na declaração dos atributos são criados
› O corpo do construtor da classe derivada é executado
• Agora vamos complicar um pouco mais
• O que acontece se, dentro de um construtor, chamarmos um método polimórfico (não static e não final) do objeto que está sendo “construído”?
› Isso pode levar a bugs muito difíceis de serem identificados
› Vamos usar o mesmo exemplo anterior, mas agora o nosso misto tem um construtor
package p2.exemplos; public class Almoco extends Refeicao { public Almoco() { System.out.println("Almoco() antes do preparo"); prepara(); System.out.println("Almoco() depois do preparo"); } public void
prepara() { System.out.println("Prepara Almoco"); } } |
package p2.exemplos; public class
AlmocoPortatil extends Almoco { public AlmocoPortatil() { System.out.println("AlmocoPortatil() antes do preparo"); prepara(); System.out.println("AlmocoPortatil() depois do preparo"); } public void prepara() { System.out.println("Prepara
AlmocoPortatil"); } } |
package p2.exemplos; public class MistoQuente extends AlmocoPortatil { private Pao pao = new Pao(); private Queijo queijo = new
Queijo(); private Presunto presunto = new
Presunto(); public MistoQuente() { System.out.println("MistoQuente() antes do preparo"); prepara(); System.out.println("MistoQuente() depois do preparo"); } public void prepara() { System.out.println("Prepara
MistoQuente"); } public static void main(String[]
args) { new MistoQuente(); } } |
•
A saída deste programa é:
Refeicao() Almoco()
antes do preparo Prepara
MistoQuente Almoco()
depois do preparo AlmocoPortatil()
antes do preparo Prepara
MistoQuente AlmocoPortatil()
depois do preparo Pao() Queijo() Presunto() MistoQuente()
antes do preparo Prepara
MistoQuente MistoQuente()
depois do preparo |
• Note
que o método prepara da subclasse esconde os métodos prepara das super-classes,
mesmo ao chamar o seus construtores (das super classes)
› Por isso, muito cuidado!
›
Não é recomendado chamar
métodos que não sejam privados dentro do construtor!!!
› Métodos privados são implicitamente final e associados a uma implementação em tempo de compilação
•
Na dúvida? Escolha a
composição
• Lembre-se que a herança estabelece uma relação “é um tipo especial de” que gera um acoplamento forte entre as classes
› Mudança na implementação da classe mãe pode requerer mudança na implementação da classe derivada e da classe que usa a classe derivada (e que talvez nem saiba que a classe mãe existe!)
› Podemos reduzir um pouco o acoplamento garantindo que todos os atributos da classe mãe são privados e objetos da classe derivada não terão acesso a eles diretamente
• Quando usamos composição, a implementação do objeto que é um membro de uma classe (atributo de uma classe) pode mudar em tempo de execução
› Isso não é possível quando usamos herança
› Veja um exemplo simples
package p2.exemplos; public class
Filho { public void
obedece() { System.out.println("Filho obedece"); } } |
package p2.exemplos; public class
FilhoChateado extends Filho { @Override public void
obedece() { System.out.println("Filho obedece chateado"); } } |
package p2.exemplos; public class
FilhoContente extends Filho { @Override public void
obedece() { System.out.println("Filho obedece contente"); } } |
package p2.exemplos; public class Mae { private Filho filho = new
FilhoContente(); private boolean contente = true; public void mudaHumorDeFilho() { if (contente) filho = new
FilhoChateado(); else filho = new
FilhoContente(); } public void
manda() { filho.obedece(); } } |
package p2.exemplos; /** * Exemplo de composição e herança com mudança do atributo em tempo de execução * e chamadas polimórficas. * * @author raquel * */ public class HoraDaEscola
{ public static void main(String[]
args) { Mae mae = new Mae(); mae.manda(); // chamada polimórfica mae.mudaHumorDeFilho(); mae.manda(); // chamada polimórfica } } |
•
A saída deste programa é:
Filho obedece contente Filho obedece chateado |