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 - @Overriderequer 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 .