O polimorfismo é a característica mais importante da OO e consiste no mecanismo pelo qual vários tipos de objetos, ou seja, formas diferentes de implementação, podem ser tratadas como sendo de um tipo comum.
Todas as linguagens orientadas a objetos possuem uma classe que constitui a raiz de sua hierarquia de tipos. A partir desta classe são criadas outras por meio da herança, a qual pode ser usada de maneira implícita ou explícita. Na plataforma Java a raiz da hierarquia é a classe java.lang.Object; enquanto que no C# a classe-raiz é System.Object.
Desta maneira, qualquer que seja a hierarquia de classes considerada, todas as classes ali existentes são subclasses de Object, sendo assim, uma variável do tipo Object pode armazenar referências de qualquer tipo de objeto.
O polimorfismo permite que um objeto seja, transparentemente, tratado como qualquer outro tipo ascendente (superclasses da qual é derivado), e também como sendo do seu tipo verdadeiro, isto é, como foi instanciado de fato. Isso possibilita a generalização, ou seja, uma operação que faz o contrário da herança, que permite a especialização.
Como vimos, um objeto cuja classe é derivada de outra é uma composição de objetos em camadas. A camada mais externa corresponde ao tipo real do objeto (por exemplo Gerente) e nela se encontram as adições providas por esta classe (sua especialização em relação à camada anterior). A camada mais interna é sempre aquela que corresponde ao tipo Object, onde estão presentes seus elementos. As camadas intermediárias são aquelas fornecidas pelos descendentes de Object até a camada mais externa. A figura que segue ilustra esta estrutura em camadas de um objeto de uma possível classe Comissionado.
Conforme o tipo da referência usada para acessar um objeto, determina-se quais camadas poderão efetivamente ser usadas. Se utilizamos uma referência do tipo real (de instanciação), temos acesso a todas os membros públicos deste tipo e de todos os tipos ancestrais (ascendentes). Na figura acima, a referência do tipo Comissionado permite acessar todos os membros públicos do objeto presentes na camada mais externa (Comissionado), na camada intermediária (Funcionario) e também na camada mais interna (Object).
Mas se a referência é do tipo Funcionario, só é possível acessar os membros desta camada (Funcionario) e das mais internas (no caso apenas Object). Uma referência de tipo Funcionário não consegue acessar nada presente em camadas mais externas do que seu tipo, porque sua classe não prevê sua existência, pois foram adicionadas nas especializações (subclasses) posteriores.
Da mesma maneira, uma referência do tipo Object só consegue acessar a porção referente a este tipo presente em objetos de qualquer classe.
Por conta da construção dos objetos em múltiplas camadas e do comportamento dos diferentes tipos de referências em relação às camadas presentes nos objetos, temos duas importantes operações decorrentes do polimorfismo:
- up type casting (ou apenas upcasting) e
- down type casting (ou apenas downcasting).
Polimorfismo e upcasting
O uptype casting, upcasting ou coerção para supertipo é a operação disponível na orientação a objetos que permite que uma referência de superclasse utilize a porção correspondente nos objetos descendentes de sua hierarquia. Em outras palavras, uma referência de um certo tipo pode acessar sua camada correspondente em qualquer objeto de tipo derivado, isto é, em qualquer subclasse.
Observe novamente a hierarquia de objetos. Funcionario (que é uma classe derivada de Object) dá origem a várias classes, tais como Caixa, Gerente, Motorista ou Comissionado.
Uma referência do tipo Gerente permite acessar todas as camadas presentes nos objetos instanciados como deste tipo, assim como apenas uma referência do tipo Comissionado permite acessar todos os elementos deste tipo, como ilustrado na figura que segue.
Referências deste tipo podem ser obtidas como no fragmento abaixo:
Gerente g2 = new Gerente(2345);
Comissionado c2 = new Comissionado(3456);
No entanto, como um Gerente é um Funcionario (é subclasse deste tipo e possui uma camada interna correspondente), uma referência do tipo Funcionario pode ser usada para acessar tal camada (e as interiores) em qualquer objeto do tipo Gerente ou Comissionado, pois é garantido que tais objetos possuem a camada interior correspondente a Funcionario.
Por meio do polimorfismo, uma referência de tipo Funcionario pode acessar objetos de tipos diferentes, embora tal acesso seja limitado a camada correspondente à referência, portanto de objetos cujos tipos são subclasses do tipo da referência. Assim é válido fazer:
// Funcionario é superclasse de Gerente
// Gerente é subclasse de Funcionario
Funcionario f2 = g2;
Funcionario f3 = new Gerente(2346);
// Funcionario é superclasse de Comissionado
// Comissionado é subclasse de Funcionario
Funcionario f4 = c2;
Funcionario f5 = new Comissionado(3457);
Aqui foi usado o upcasting, ou seja, uma referência de um tipo ancestral (Funcionario) foi usada para acessar a camada correspondente em objetos de seus subtipos (Gerente e Comissionado). Assim, objetos de tipos diferentes são acionados por um mesmo conjunto de métodos, ou seja, recebem os mesmos tipos de mensagens. Esta situação é ilustrada na figura que segue.
Quando o tipo de referência usada não é compatível com a hierarquia do objeto, o compilador sinaliza o erro e impede que o código seja compilado, evitando que objetos sejam incorretamente manipulados por referências inadequadas.
Já referências de tipo Object, por meio do upcasting, podem acessar qualquer tipo de objeto, pois qualquer tipo é subclasse de Object e todo objeto possui como camada mais interior aquela que corresponde a Object, como mostra a figura que segue.
Assim temos que é possível fazer:
Object o1 = new Gerente(2367);
Object o2 = new Comissionado(3489);
Object o3 = f2;
Object o4 = f5;
Object o5 = new String("Upcasting");
Isto significa que, sob certos aspectos, uma operação de up type casting é como uma operação implícita de coerção (conversão de tipo) que possibilita o uso mais genérico de um objeto, mas só é válida quando utiliza subclasse do tipo real do objeto.
O upcasting é um mecanismo polimórfico de generalização, que permite o uso de um objeto por meio de referências de seus supertipos, embora as operações estejam limitadas as características da superclasse usada. Isto é conveniente para tratar famílias de objetos, pois uma referência de superclasse permite manipular qualquer um de seus subtipos, ao invés de requerer referências de tipos específicos.
Isto flexibiliza o armazenamento de objetos em arrays e outras estruturas de dados; possibilita que métodos retornem objetos de tipos diferentes (mas de uma mesma família; além de facilitar alterações no projeto de sistemas.
Por exemplo, no fragmento de código que segue, um array de tipo Funcionario pode conter objetos de qualquer tipo derivado de Funcionario:
// cria array de Funcionario
Funcionario folha[] = new Funcionario[3];
// instancia e ajusta objeto tipo funcionário
Funcionario f = new Funcionario(1234);
f.setSalarioBase(850);
// atribui objeto Funcionario ao array de mesmo tipo
folha[0] = f;
// instancia e ajusta objeto tipo Comissionado
Comissionado c = new Comissionado(2345);
f.setSalarioBase(650);
// atribui objeto Comissionado ao array
folha[1] = c;
// instancia objeto Gerente, atribuindo-o ao array, ajustando-o
folha[2] = new Gerente(3456);
folha[2].setSalarioBase(4500);
Nos elementos [1] e [2] do array, cujo tipo declarado é Funcionario, ocorre o upcasting, pois objetos de subclasses de Funcionario são ali armazenados.
O uso do array de Funcionario permite generalizar o tratamento de funcionários de tipos diferentes, como no outro fragmento que segue:
double folhaBaseTotal = 0;
for(int i=0; i<folha.length; i++) {
folhaBaseTotal += folha[i].getSalarioBase();
}
System.out.println("Folha total = " + folhaBaseTotal);
No laço existente, a despeito do tipo real, todas as características do tipo Funcionario estão disponíveis por meio das referências existentes no array de tipo Funcionario. No entanto, ao acionar método getSalarioBase(), disponível na interface do tipo Funcionario, é de fato acionada a implementação existente na classe efetiva do objeto armazenado naquela posição, pois esta substitui (method overriding) a definida em Funcionario. Assim, este laço de tratamento generalizado acaba usado a operação definida nas subclasses de Funcionario, sem qualquer tratamento condicional.
O upcasting permite, assim, criar operações genéricas para lidar com múltiplos tipos de objetos, relacionados numa hierarquia de classes por meio de um ancestral comum, como segue:
double getFolhaBaseTotal(Funcionario[] folha ){
double folhaBaseTotal = 0;
for(int i=0; i<folha.length; i++) {
folhaBaseTotal += folha[i].getSalarioBase();
}
return folhaBaseTotal;
}
O método getFolhaBaseTotal(Funcionario[]) recebe um array de tipo Funcionario. Como antes, a despeito do tipo real, todas as características do tipo Funcionario estão disponíveis por meio das referências existentes no array de tipo Funcionario. Mas, dada a sobreposição de métodos, serão usadas as implementações dos tipos específicos. Então, mesmo que surjam novos subtipos derivados de Funcionario, esta operação continuará a funcionar, sem necessidade de qualquer alteração.
Polimorfismo e downcasting
O polimorfismo também dispõe de uma operação que permite converter uma referência de superclasse em outra de subclasse, portanto mais especializada, que é chamada de down type casting, downcasting ou coerção para subtipo.
Como mostra a figura, o downcasting é uma operação inversa ao upcasting. Enquanto o upcasting ocorre quando se utiliza uma referência de supertipo (ancestral) para o tratamento de um objeto; no downcasting é usada uma referência de subtipo (descendente) para o tratamento de um objeto.
Aqui, a referência desejada como resultado do downcasting passa a utilizar uma camada mais externa do objeto, decorrente da especialização. Mas como o compilador não pode garantir que o objeto referenciado possua tais camadas adicionais, esta operação deve ser explicitamente indicada pelo programador, como uma espécie de autorização para sua realização.
Observe no fragmento de código que segue, como uma referência f3, de tipo Funcionario ou mesmo Object, é transformada em um objeto de tipo Comissionado (que é uma de subclasses); ou como uma referência de Object é transformada em outra do tipo String:
Comissionado c = (Comissionado) f3;
String str = (String) obj;
A transformação de tipo, down type casting ou coerção possibilita transformar uma referência do objeto em outra de seu tipo real ou de tipo mais próximo. Como nem sempre isso será possível, porque o objeto não foi, de fato, criado como algo daquele tipo, a compilação do código requer a explicitação da conversão por meio da indicação do tipo de destino entre parêntesis.
Se a referência não pode ser transformada na tipo indicado, o downcasting provoca o lançamento da exceção ClassCastException, como nestes exemplos inválidos:
// Upcasting OK
Funcionario f4 = new Gerente (6789);
// Downcasting INVALIDO
Comissionado c = (Comissionado) f4;
Para evitar esse tipo de exceção, existe o operador instanceof.
Operador instanceof
O operador especial instanceof, que retorna um resultado booleano, permite verificar, em tempo de execução, se um objeto é de um tipo específico ou de suas subclasses. A sua sintaxe é:
<objeto> instanceof <Tipo>
O uso deste operador retorna true, quando o objeto é uma instância direta do tipo indicado ou de alguma de suas subclasses; ou false, quando o objeto não é uma instancia do tipo indicado ou de qualquer uma de suas subclasses.
O uso do operador instanceof permite evitar erros em operações de downcasting, mas sua aplicação não pode ser generalizada, devendo empregar tipos específicos. Um exemplo:
Funcionario func = new Gerente(5678);
if (func instanceof Gerente) {
Gerente g = (Gerente) func; // downcasting
System.out.println("Gerente depto: " + g.getGerencia() );
}
Outro exemplo:
Integer n = new Integer (123456789);
if (n instanceof Number) {
count= n.intValue();
}
Com o uso de instanceof é possível garantir a execução de operações de downcasting sem a ocorrência de exceções ClassCastException decorrentes de coerção inválida de objetos.
Considerações finais
O polimorfismo é o mecanismo principal da OO que pode se manifestar tanto em tempo de compilação, quanto em tempo de execução.
Em tempo de compilação o polimorfismo aparece como a sobrecarga de métodos (method overload), também como sobrecarga de operadores em algumas linguagens OO, também sendo chamado de ligação precoce (early binding). Já em tempo de execução, aparece como sobreposição de métodos (method overriding), que é decorrência do uso do mecanismo da herança onde a versão mais especializada de um método, presente em subclasses, é acionada, situação conhecida como ligação tardia (late binding).
Assim, com o polimorfismo é possível:
- Que subclasses forneçam implementações diferentes de métodos com uma mesma assinatura (ou seja, a sobreposição de métodos ou method overriding), provendo então comportamentos distintos, modificados em cada classe derivada conforme a especialização desejada.
- Que referências de superclasse acionem métodos de subclasse comuns à superclasse (sobrepostos) em tempo de execução, ou seja, invoquem métodos diferentes, mas com a mesma assinatura, possibilitando que tipos diferentes recebam as mesmas mensagens.
Finalmente, grande parte da flexibilidade e elegância das linguagens de programação OO decorrem do polimorfismo.
Referências Bibliográficas
[1] JAMSA, K.; KLANDER, L.. Programando em C/C++: a bíblia. São Paulo: Makron Books, 1999.
[2] PAGE_JONES, M.. Fundamentos do Desenho Orientado a Objeto com UML. São Paulo: Makron Books, 2001.
[3] SOMMERVILLE, I.. Software Engineering. 6th. Ed. Harlow: Pearson, 2001.
[4] DEITEL, H.M.; DEITEL, P.J.. Java: como programar. 6a. Ed. São Paulo: Pearson Prentice-Hall, 2005.
[5] SAVITCH, W.. C++ Absoluto. São Paulo: Pearson Addison-Wesley, 2004.
[6] JANDL JR., P. Introdução ao C++. São Paulo: Futura, 2003.
[7] JANDL JR., P.. Java - guia do programador. 3a. ed. São Paulo: Novatec, 2015.
[8] RUMBAUGH, J.; BLAHA, M.; PREMERLANI, W.; EDDY, F.; LORENSEN, W.. Object-oriented modeling and design. Englewoods Cliffs: Prentice-Hall, 1991.
[9] STROUSTRUP, B.. The C++ Programming Language. 3rd Ed. Reading: Addison-Wesley, 1997.
[10] LANGSAM, Y.; AUGENSTEIN, M. J.; TENENBAUM, A. M.. Data structures using C and C++. 2nd Ed. Upper Saddle River: Prentice-Hall, 1996.
[11] WATSON, K.; NAGEL, C.; PEDERSEN, J.H.; REID, J.D.; SKINNER, M.; WHITE, E.. Beginning Microsoft Visual C# 2008. Indianapolis: Wiley Publishing, 2008.
[12] GAMMA, E.; HELM, R.; JOHNSON, R.; VLISSIDES, J.. Design Patterns: Elements of Reusable Object-Oriented Software. Reading, MA: Addison-Wesley, 1995.