Java: Entendendo Polimorfismo

Java: Entendendo Polimorfismo

Java suporta polimorfismo, a propriedade de um objeto assumir diferentes formas. Mais precisamente, um objeto Java pode ser acessado usando a referência com o mesmo tipo como um objeto, uma referência que é a superclasse do objeto, ou uma referência que define uma interface que o objeto implementa, tanto diretamente ou através de uma superclasse.

Além disso, um cast não é necessário se o objeto é transferido para um super tipo ou a interface do objeto.


Para este artigo, você precisa entender o seguinte:

  • Uma interface pode definir métodos abstratos;
  • Uma classe pode implementar qualquer número de interfaces;
  • Uma classe implementa uma interface sobrescrevendo os métodos abstratos herdados;
  • Um objeto que implementa uma interface pode ser atribuído como referência desta interface.

Vamos ilustrar esta propriedade do polimorfismo com o seguinte exemplo:


Este código compila e imprime o seguinte output:


A coisa mais importante a notar sobre esse exemplo é que apenas um objeto, Lemur, é criado e referenciado. O polimorfismo habilita uma instância de Lemur a ser transferida ou passada para um método usando um de seus supertipos, tais como Primate ou HasTail.

Uma vez que o objeto foi atribuído a um novo tipo de referência, somente os métodos e variáveis estarão disponíveis a um novo tipo de referência, eles são aptos a serem chamados no objeto sem um cast explícito. Por exemplo, os seguintes trechos de código não irão compilar:


Neste exemplo, a referência hasTail tem acesso direto somente aos métodos definidos com o HasTail interface; portanto, ele não sabe que a variável age é parte do objeto. Da mesma maneira, a referência primate possui acesso somente a métodos definidos na classe Primate, e ela não possui acesso direto ao método isTailStriped().

Objeto vs. Referência

Em Java, todos os objetos são acessados por referência, portanto, como desenvolvedor, você nunca tem acesso direto ao próprio objeto. Conceitualmente, no entanto, você deve considerar o objeto como uma entidade que existe em memória, alocado no ambiente de runtime do Java. Sem considerar que o tipo de referência do objeto que você tem em memória, o objeto por ele próprio não muda. Por exemplo, todos os objetos herdam de java.lang.Object, todos eles podem ser atribuídos ao java.lang.Object, como mostrado no seguinte exemplo:


Embora o objeto Lemur tenha sido atribuído como uma referência com um tipo diferente, o objeto por si só não foi alterado e continua a existir como um objeto Lemur em memória. O que tem sido alterado, então, é nossa habilidade de acessar métodos dentro da classe Lemur com a referência lemurAsObject. Sem um cast explícito de volta a Lemur, nós não temos mais acesso às propriedades do objeto.

Nós podemos sumarizar este princípio com as duas seguintes regras:

  • O tipo do objeto determina quais propriedades existem dentro do objeto em memória;
  • O tipo da referência do objeto determina quais métodos e variáveis são acessíveis ao programa Java.

Portanto, uma mudança bem sucedida em uma referência de um objeto para um novo tipo de referência pode dar a você acesso às novas propriedades do objeto, mas lembre-se, aquelas propriedades existiam antes da mudança de referência ocorrer.

Dependendo do tipo de referência, nós podemos apenas ter acesso a certos métodos. Por exemplo, a referência hasTail tem acesso ao método isTailStriped() mas não tem acesso à variável age, definida na classe Lemur.

Casting de Objetos (Objects)

No exemplo anterior, nós criamos uma instância única de um objeto Lemur e acessamos ele via referências de superclasse e interface. Uma vez que mudamos o tipo de referência, no entanto, nós perdemos acesso a membros mais específicos definidos na subclasse que ainda existem dentro do objeto. Nós podemos recuperar aquelas referências fazendo um cast do objeto de volta para a subclasse específica de onde ele veio.


Neste exemplo, nós primeiro criamos um objeto Lemur e implicitamente fazemos um cast dele para uma referência Primata. Como Lemur é uma subclasse de Primate, isto pode ser feito sem um operador de cast. Depois, nós tentamos converter a referência primate de volta a uma referência lemur, lemur2, sem um cast explícito.

O resultado é que o código não irá compilar. No segundo exemplo, no entanto, nós fazemos o cast explicitamente para uma subclasse do objeto Primate, e nós ganhamos acesso a todos os métodos e campos disponíveis da classe Lemur.

Quando você faz casting de objetos, você não precisa de um operador de cast se a atual referência é um subtipo do tipo de destino. Isso é referido a um cast implícito ou conversão de tipo. Alternativamente, se a referência atual não é um subtipo do tipo de destino, então você precisa realizar um cast explícito com um tipo compatível. Se o objeto subjacente não é compatível com o tipo, então uma ClassCastException será lançada em tempo de execução.

Em resumo, estes conceitos podem ser explicados nas seguintes regras:

  • Realizando cast de uma referência de um subtipo para um supertipo não requer um cast explícito;
  • Realizando um cast de uma referência de um supertipo para um subtipo requer um cast explícito;
  • O compilador não permite cast para uma classe não relacionada;
  • Em runtime, um cast invalido de uma referência para um tipo não relacionado resulta em uma ClassCastException sendo lançada.


Neste exemplo, as classes Fish e Bird não são relacionadas através de nenhuma hierarquia de classe que o compilador tem conhecimento; portanto, o código não vai compilar. Enquanto ambos estendem Object implicitamente, eles são considerados tipos não relacionados uma vez que eles não podem ser subtipos um do outro.

Embora duas classes compartilhem a hierarquia relacionada, isto não quer dizer que uma instância de uma pode automaticamente fazer cast para outra. Vejamos um exemplo:


Este código cria uma instância do Rodent e então tenta fazer o cast dele para uma subclasse de Rodent, Capybara. No entanto, este código não vai compilar, ele vai lançar uma ClassCastException. É importante ter em mente neste exemplo, que o objeto criado (Rodent) não herda da classe Capybara de nenhuma maneira.

The instanceof Operator

O operador instanceof pode ser usado para checar se um objeto pertence a uma classe ou interface particular e a prevenir ClassCastExceptions em runtime. Diferentemente do código anterior, o código seguinte não lança uma exception em tempo de execução e executa o cast somente se o operador instanceof retorna true:


Assim como o compilador não permite fazer o casting de um objeto a tipos não relacionados, ele também não permite usar o instanceof com tipos não relacionados. Nós podemos demonstrar isso com nossas classes não relacionadas: Bird e Fish:

Neste trecho de código, nem o operador instanceof, tampouco a operação de cast explícito compilam.

Polimorfismo e Sobrescrita (Overriding) de Método

Em Java, o polimorfismo afirma que quando você sobrescreve um método, você substitui todas as chamadas a ele, até mesmo aquelas definidas na classe pai. Como exemplo, o que você pensa dos outputs do trecho de código abaixo?


Neste exemplo, o objeto sendo operado em memória é o EmperorPenguin. O método getHeight é sobrescrito na subclasse, significando que todas as chamadas a esse método serão substituídas em runtime. Apesar de que o método printInfo() sendo definido na classe Penguin, chamando o getHeight no objeto, chama método associado com o objeto preciso, em memória, não a referência corrente do tipo onde ele é chamado. Mesmo usando a referência this, a qual é opcional neste exemplo, ela não chama a classe pai porque o método foi substituído.

A faceta do polimorfismo que substitui métodos via sobrescrita é uma das mais importantes propriedades em todo o Java. Ela permite você criar complexas estruturas de herança, com subclasses que têm suas próprias implementações customizadas de métodos sobrescritos. Elas significam que a classe pai não necessita ser atualizada para usar a customizada ou a sobrescrita de método. Se o método é propriamente sobrescrito, então a versão sobrescrita vai ser usada em lugares que ele for chamado. Lembre-se, você pode escolher limitar o comportamento polimórfico declarando métodos com final, o qual previne eles de serem sobrescritos por uma subclasse.

Sobrescrita vs. Membros escondidos (Hiding)

Enquanto sobrescrita de métodos substitui o método em todo lugar em que é chamado, método estático (static) e variável escondida (hiding) não substitui. Falando estritamente, métodos escondidos não são uma forma de polimorfismo desde que métodos e variáveis mantém suas propriedades individuais. Diferentemente da sobrescrita de método, membros escondidos são bastante sensíveis a tipos de referência e localização onde os membros estão sendo usados. Vamos dar uma olhada neste exemplo:


O exemplo CrestedPenguin é praticamente idêntico ao nosso exemplo anterior, EmperorPenguin, entretanto, como você provavelmente já adivinhou, ele imprime 3 em vez de 8. O método getHeight() é estático e é portanto escondido (hidden), não sobrescrito. O resultado é que chamando getHeight() em CrestedPeguin retorna um valor diferente do que chamando ele em Penguin, mesmo se o objeto subjacente é o mesmo. Contrastando este com um método sobrescrito, onde ele retorna o mesmo valor para um objeto sem considerar qual classe está sendo invocada.

De fato, o compilador vai avisar você quando você acessar membros estáticos de uma maneira não estática. Neste caso, a referência this não terá nenhum impacto no output do programa.

Além da localização, o tipo de referência pode também determinar o valor que você recupera quando você está trabalhando com membros escondidos (hidden). Vamos dar uma olhada em um exemplo mais complexo:


A saída desse programa é a seguinte:


Relembre, neste exemplo, apenas um objeto do tipo Kangaroo, é criado e armazenado em memória. Desde que métodos estáticos podem apenas ser escondidos (hidden), não sobrescritos. Java usa o tipo de referência para determinar qual versão do isBiped() deverá ser chamada, resultando em joey.isBiped() imprimindo true e moey.isBiped() imprimindo false.

Da mesma maneira, a variável é escondida (hidden), não sobrescrita, então o tipo da referência é usado para determinar qual valor retornar como output. Isto resulta em joey.age retornando 6 e moey.age retornando 2.

Espero que este artigo tenha sido útil para entender como funciona o polimorfismo.

Até a próxima!

💡
As opiniões e comentários expressos neste artigo são de propriedade exclusiva de seu autor e não representam necessariamente o ponto de vista da Revelo.

A Revelo Content Network acolhe todas as raças, etnias, nacionalidades, credos, gêneros, orientações, pontos de vista e ideologias, desde que promovam diversidade, equidade, inclusão e crescimento na carreira dos profissionais de tecnologia.