Desafiadores de Java # 3: Polimorfismo e herança
Continuamos a traduzir uma série de artigos com problemas de Java. O último post sobre as linhas causou uma discussão surpreendentemente acalorada . Esperamos que você também não passe por este artigo. E sim - agora estamos convidando você para o décimo aniversário do nosso curso Java Developer .
De acordo com o lendário Venkat Subramaniam, o polimorfismo é o conceito mais importante na programação orientada a objetos. Polimorfismo - ou a capacidade de um objeto de executar ações especializadas com base em seu tipo - é o que torna o código Java flexível. Padrões de design, como o Comando, o Observador, o Decorador, a Estratégia e muitos outros criados pelo Gangue dos Quatro, todos usam alguma forma de polimorfismo. Dominar esse conceito melhorará muito sua capacidade de pensar em soluções de software.

Você pode pegar o código fonte deste artigo e experimentar aqui: https://github.com/rafadelnero/javaworld-challengers
Interfaces e herança no polimorfismo
Neste artigo, focaremos na relação entre polimorfismo e herança. O principal a ter em mente é que o polimorfismo requer herança ou implementação de uma interface . Você pode ver isso no exemplo abaixo com Duke e Juggy
:
public abstract class JavaMascot { public abstract void executeAction(); } public class Duke extends JavaMascot { @Override public void executeAction() { System.out.println("Punch!"); } } public class Juggy extends JavaMascot { @Override public void executeAction() { System.out.println("Fly!"); } } public class JavaMascotTest { public static void main(String... args) { JavaMascot dukeMascot = new Duke(); JavaMascot juggyMascot = new Juggy(); dukeMascot.executeAction(); juggyMascot.executeAction(); } }
A saída deste código será assim:
Punch! Fly!
Como implementações específicas são definidas, os métodos Duke
e Juggy
serão chamados.
O método sobrecarrega um polimorfismo? Muitos programadores confundem a relação do polimorfismo com a substituição e a sobrecarga . De fato, apenas redefinir o método é polimorfismo verdadeiro. A sobrecarga usa o mesmo nome do método, mas parâmetros diferentes. Polimorfismo é um termo amplo, portanto sempre haverá discussões sobre esse tópico.
Qual é o objetivo do polimorfismo
Uma grande vantagem e objetivo de usar o polimorfismo é reduzir a conexão da classe cliente com a implementação. Em vez de código fixo, a classe cliente obtém uma implementação da dependência para executar a ação necessária. Portanto, a classe cliente sabe o mínimo para concluir suas ações, que é um exemplo de ligação fraca.
Para entender melhor o objetivo do polimorfismo, dê uma olhada no SweetCreator
:
public abstract class SweetProducer { public abstract void produceSweet(); } public class CakeProducer extends SweetProducer { @Override public void produceSweet() { System.out.println("Cake produced"); } } public class ChocolateProducer extends SweetProducer { @Override public void produceSweet() { System.out.println("Chocolate produced"); } } public class CookieProducer extends SweetProducer { @Override public void produceSweet() { System.out.println("Cookie produced"); } } public class SweetCreator { private List<SweetProducer> sweetProducer; public SweetCreator(List<SweetProducer> sweetProducer) { this.sweetProducer = sweetProducer; } public void createSweets() { sweetProducer.forEach(sweet -> sweet.produceSweet()); } } public class SweetCreatorTest { public static void main(String... args) { SweetCreator sweetCreator = new SweetCreator(Arrays.asList( new CakeProducer(), new ChocolateProducer(), new CookieProducer())); sweetCreator.createSweets(); } }
Neste exemplo, você pode ver que a classe SweetCreator
conhece SweetCreator
a classe SweetProducer
. Ele não conhece a implementação de todo Sweet
. Essa separação nos dá a flexibilidade de atualizar e reutilizar nossas classes, e isso facilita muito a manutenção do código. Ao projetar o código, sempre procure maneiras de torná-lo o mais flexível e conveniente possível. O polimorfismo é uma maneira muito poderosa de ser usada para esse fim.
A @Override
requer que o programador use a mesma assinatura de método, que deve ser redefinida. Se o método não for substituído, haverá um erro de compilação.
Tipos de retorno covariantes ao substituir um método
Você pode alterar o tipo de retorno de um método substituído, se for do tipo covariante . O tipo covariante é basicamente uma subclasse do valor de retorno.
Considere um exemplo:
public abstract class JavaMascot { abstract JavaMascot getMascot(); } public class Duke extends JavaMascot { @Override Duke getMascot() { return new Duke(); } }
Como Duke
é JavaMascot
, podemos alterar o tipo do valor de retorno ao substituir.
Polimorfismo nas classes base Java
Usamos constantemente o polimorfismo nas classes Java básicas. Um exemplo muito simples é instanciar uma classe ArrayList
com uma declaração de tipo como uma interface de List
.
List<String> list = new ArrayList<>();
Considere um código de amostra que usa a API Java Collections sem polimorfismo:
public class ListActionWithoutPolymorphism {
Código nojento, certo? Imagine que você precisa acompanhá-lo! Agora considere o mesmo exemplo com o polimorfismo:
public static void main(String... polymorphism) { ListAction listAction = new ListAction(); listAction.executeListActions(); } public class ListAction { void executeListActions(List<Object> list) {
A vantagem do polimorfismo é flexibilidade e extensibilidade. Em vez de criar vários métodos diferentes, podemos declarar um método que obtém um tipo de List
.
Chamando métodos específicos para um método polimórfico
Você pode chamar métodos específicos com uma chamada de método polimórfica, isso é devido à flexibilidade. Aqui está um exemplo:
public abstract class MetalGearCharacter { abstract void useWeapon(String weapon); } public class BigBoss extends MetalGearCharacter { @Override void useWeapon(String weapon) { System.out.println("Big Boss is using a " + weapon); } void giveOrderToTheArmy(String orderMessage) { System.out.println(orderMessage); } } public class SolidSnake extends MetalGearCharacter { void useWeapon(String weapon) { System.out.println("Solid Snake is using a " + weapon); } } public class UseSpecificMethod { public static void executeActionWith(MetalGearCharacter metalGearCharacter) { metalGearCharacter.useWeapon("SOCOM");
A técnica que usamos aqui é converter ou alterar conscientemente o tipo de um objeto em tempo de execução.
Observe que uma chamada para um método específico é possível apenas ao converter um tipo mais geral para um tipo mais específico. Uma boa analogia seria dizer explicitamente ao compilador: "Ei, eu sei o que estou fazendo aqui, então vou converter o objeto para um tipo específico e usarei esse método".
Referindo-se ao exemplo acima, o compilador tem um bom motivo para não aceitar a chamada de certos métodos: a classe que é passada deve ser SolidSnake
. Nesse caso, o compilador não tem como garantir que todas as subclasses de MetalGearCharacter
tenham um método giveOrderToTheArmy
.
Instância da palavra-chave
Observe a palavra reservada instanceof
. Antes de chamar um método específico, perguntamos se MetalGearCharacter
instância do BigBoss
. Se essa não é uma instância do BigBoss
, obteremos a seguinte exceção:
Exception in thread `main" java.lang.ClassCastException: com.javaworld.javachallengers.polymorphism.specificinvocation.SolidSnake cannot be cast to com.javaworld.javachallengers.polymorphism.specificinvocation.BigBoss
Palavra super
chave super
E se quisermos nos referir a um atributo ou método da classe pai? Nesse caso, podemos usar a super
palavra super
chave.
Por exemplo:
public class JavaMascot { void executeAction() { System.out.println("The Java Mascot is about to execute an action!"); } } public class Duke extends JavaMascot { @Override void executeAction() { super.executeAction(); System.out.println("Duke is going to punch!"); } public static void main(String... superReservedWord) { new Duke().executeAction(); } }
O uso da palavra reservada super
no método executeAction
da classe Duke
chama o método da classe pai. Em seguida, executamos uma ação específica da classe Duke
. É por isso que podemos ver as duas mensagens na saída:
The Java Mascot is about to execute an action! Duke is going to punch!
Resolver o problema do polimorfismo
Vamos verificar o que você aprendeu sobre polimorfismo e herança.
Nesta tarefa, você recebe vários métodos de Os Simpsons, de Matt Groening, dos Wavs necessários para desvendar qual será o resultado de cada classe. Primeiro, analise cuidadosamente o seguinte código:
public class PolymorphismChallenge { static abstract class Simpson { void talk() { System.out.println("Simpson!"); } protected void prank(String prank) { System.out.println(prank); } } static class Bart extends Simpson { String prank; Bart(String prank) { this.prank = prank; } protected void talk() { System.out.println("Eat my shorts!"); } protected void prank() { super.prank(prank); System.out.println("Knock Homer down"); } } static class Lisa extends Simpson { void talk(String toMe) { System.out.println("I love Sax!"); } } public static void main(String... doYourBest) { new Lisa().talk("Sax :)"); Simpson simpson = new Bart("D'oh"); simpson.talk(); Lisa lisa = new Lisa(); lisa.talk(); ((Bart) simpson).prank(); } }
O que você acha? Qual será o resultado? Não use o IDE para descobrir! O objetivo é melhorar suas habilidades de análise de código, então tente decidir por si mesmo.
Escolha sua resposta (você pode encontrar a resposta correta no final do artigo).
A)
Eu amo sax!
D'oh
Simpson!
D'oh
B)
Sax :)
Coma minha bermuda!
Eu amo sax!
D'oh
Bata no local
C)
Sax :)
D'oh
Simpson!
Bata no local
D)
Eu amo sax!
Coma minha bermuda!
Simpson!
D'oh
Bata no local
O que aconteceu Entendendo o polimorfismo
Para a seguinte chamada de método:
new Lisa().talk("Sax :)");
a saída será "Eu amo Sax!". Isso ocorre porque passamos a string para o método e a classe Lisa
possui esse método.
Para a próxima chamada:
Simpson simpson = new Bart("D'oh"); simpson.talk();
A saída será "Eat my shorts!". Isso ocorre porque inicializamos o tipo Simpson
com Bart
.
Agora veja, isso é um pouco mais complicado:
Lisa lisa = new Lisa(); lisa.talk();
Aqui usamos método sobrecarregando com herança. Como não passamos nada para o método de talk
, o método de talk
de Simpson
chamado.
Nesse caso, a saída será "Simpson!".
Aqui está outro:
((Bart) simpson).prank();
Nesse caso, a sequência de prank
foi passada ao instanciar a classe Bart
por meio do new Bart("D'oh");
. Nesse caso, o método super.prank()
é chamado primeiro e, em seguida, o método prank()
da classe Bart
. A conclusão será:
"D'oh" "Knock Homer down"
Erros comuns de polimorfismo
Um erro comum é pensar que você pode chamar um método específico sem usar conversão de tipo.
Outro erro é a incerteza sobre qual método será chamado ao instanciar polimorficamente a classe. Lembre-se de que o método que está sendo chamado é o método da instância instanciada.
Lembre-se também de que substituir um método não é uma sobrecarga de um método.
Não é possível substituir o método se os parâmetros forem diferentes. Você pode alterar o tipo de retorno de um método substituído se o tipo de retorno for uma subclasse.
O que você precisa se lembrar sobre o polimorfismo
A instância criada determina qual método será chamado ao usar o polimorfismo.
A @Override
requer que o programador use um método substituído; caso contrário, ocorrerá um erro do compilador.
O polimorfismo pode ser usado com classes regulares, classes abstratas e interfaces.
A maioria dos padrões de design depende de alguma forma de polimorfismo.
A única maneira de chamar seu método em uma subclasse polimórfica é usar conversão de tipo.
Você pode criar uma estrutura de código poderosa usando polimorfismo.
Experiência. Com isso, você será capaz de dominar esse poderoso conceito!
A resposta
A resposta é D.
A conclusão será:
I love Sax! Eat my shorts! Simpson! D'oh Knock Homer down
Como sempre, congratulo-me com seus comentários e perguntas. E estamos esperando Vitaly em uma lição aberta .