Java Challengers # 3: Polymorphism and Inheritance

Original author: Rafael Chinelato Del Nero
  • Transfer

Java Challengers # 3: Polymorphism and Inheritance


We continue to translate a series of articles with puzzles on Java. The last post about the line caused a surprisingly heated discussion . We hope that you will not pass by this article either. And yes - we now invite to the anniversary tenth stream of our Java Developer course .


According to the legendary Venkat Subramaniam, polymorphism is the most important concept in object-oriented programming. Polymorphism — or the ability of an object to perform specialized actions based on its type — is what makes Java code flexible. Design patterns, such as Command (Command), Observer, Decorator, Strategy, and many others created by the gang of four, all use some form of polymorphism. Mastering this concept will greatly improve your ability to think through software solutions.



You can get the source code for this article and experiment here: https://github.com/rafadelnero/javaworld-challengers


Interfaces and inheritance in polymorphism


In this article, we focus on the relationship between polymorphism and inheritance. The main thing to keep in mind that polymorphism requires inheritance or implementation of the interface . You can see this in the example below with Duke ( Duke) and Jaggy ( Juggy):


publicabstractclassJavaMascot{
  publicabstractvoidexecuteAction();
}
publicclassDukeextendsJavaMascot{
  @OverridepublicvoidexecuteAction(){
    System.out.println("Punch!");
  }
}
publicclassJuggyextendsJavaMascot{
  @OverridepublicvoidexecuteAction(){
     System.out.println("Fly!");
  }
}
publicclassJavaMascotTest{
  publicstaticvoidmain(String... args){
    JavaMascot dukeMascot = new Duke();
    JavaMascot juggyMascot = new Juggy();
    dukeMascot.executeAction();
    juggyMascot.executeAction();
  }
}

The output of this code will be as follows:


Punch!
Fly!

Since specific implementations are defined, methods and Dukeand will be invoked Juggy.


Is overloading the method polymorphism? Many programmers confuse a polymorphism relationship with overriding methods and overloading methods . In fact, only redefinition of the method is true polymorphism. Overload uses the same method name, but different parameters. Polymorphism is a broad term, so there will always be discussions on this topic.


What is the purpose of polymorphism


The big advantage and purpose of using polymorphism is to reduce the client class relatedness with the implementation. Instead of hardcoding, the client class gets the dependency implementation to perform the necessary action. Thus, the client class knows the minimum to perform its actions, which is an example of weak binding.


To better understand the purpose of polymorphism, take a look at SweetCreator:


publicabstractclassSweetProducer{
  publicabstractvoidproduceSweet();
}
publicclassCakeProducerextendsSweetProducer{
  @OverridepublicvoidproduceSweet(){
    System.out.println("Cake produced");
  }
}
publicclassChocolateProducerextendsSweetProducer{
  @OverridepublicvoidproduceSweet(){
    System.out.println("Chocolate produced");
  }
}
publicclassCookieProducerextendsSweetProducer{
  @OverridepublicvoidproduceSweet(){
    System.out.println("Cookie produced");
  }
}
publicclassSweetCreator{
  private List<SweetProducer> sweetProducer;
  publicSweetCreator(List<SweetProducer> sweetProducer){
    this.sweetProducer = sweetProducer;
  }
  publicvoidcreateSweets(){
    sweetProducer.forEach(sweet -> sweet.produceSweet());
  }
}
publicclassSweetCreatorTest{
  publicstaticvoidmain(String... args){
    SweetCreator sweetCreator = new SweetCreator(Arrays.asList(
      new CakeProducer(),
      new ChocolateProducer(), 
      new CookieProducer()));
     sweetCreator.createSweets();
  }
}

In this example, you can see that the class SweetCreatorknows only about the class SweetProducer. He does not know the realization of everyone Sweet. This separation gives us the flexibility to update and reuse our classes, and this makes the code much easier to maintain. When designing a code, always look for ways to make it as flexible and convenient as possible. Polymorphism is a very powerful way to use for this purpose.


The annotation @Overrideobliges the programmer to use the same method signature that needs to be redefined. If the method is not overridden, there will be a compilation error.

Covariant return types when overriding a method


You can change the return type of the overridden method if it is a covariant type . The covariant type is basically a subclass of the return value.


Consider an example:


publicabstractclassJavaMascot{
  abstract JavaMascot getMascot();
}
publicclassDukeextendsJavaMascot{
  @OverrideDuke getMascot(){
    returnnew Duke();
  }
}

Because it Dukeis JavaMascot, we can change the type of the return value when overriding.


Polymorphism in Java Base Classes


We constantly use polymorphism in base Java classes. One very simple example is creating an instance of a class ArrayListwith a type declaration as an interface List.


List<String> list = new ArrayList<>();

Consider sample code using the Java Collections API without polymorphism:


publicclassListActionWithoutPolymorphism{
  // Пример без полиморфизма voidexecuteVectorActions(Vector<Object> vector){/* Здесь повтор кода */}
  voidexecuteArrayListActions(ArrayList<Object> arrayList){/* Здесь повтор кода */}
  voidexecuteLinkedListActions(LinkedList<Object> linkedList){/* Здесь повтор кода */}
  voidexecuteCopyOnWriteArrayListActions(CopyOnWriteArrayList<Object> copyOnWriteArrayList){ /* Здесь повтор кода */}
}
publicclassListActionInvokerWithoutPolymorphism{
  listAction.executeVectorActions(new Vector<>());
  listAction.executeArrayListActions(new ArrayList<>());
  listAction.executeLinkedListActions(new LinkedList<>());
  listAction.executeCopyOnWriteArrayListActions(new CopyOnWriteArrayList<>());
}

Disgusting code, right? Imagine that you need to accompany him! Now consider the same example with polymorphism:


publicstaticvoidmain(String... polymorphism){
ListAction listAction = new ListAction();   
  listAction.executeListActions();
}
publicclassListAction{
  voidexecuteListActions(List<Object> list){
    // Выполнение действий с различными списками
  }
}
publicclassListActionInvoker{
  publicstaticvoidmain(String... masterPolymorphism){
    ListAction listAction = new ListAction();
    listAction.executeListActions(new Vector<>());
    listAction.executeListActions(new ArrayList<>());
    listAction.executeListActions(new LinkedList<>());
    listAction.executeListActions(new CopyOnWriteArrayList<>());
  }
}

The advantage of polymorphism is flexibility and extensibility. Instead of creating several different methods, we can declare one method that gets the type List.


Calling specific methods for the polymorphic method


You can call specific methods in a polymorphic method call, this is due to flexibility. Here is an example:


publicabstractclassMetalGearCharacter{
    abstractvoiduseWeapon(String weapon);
}
publicclassBigBossextendsMetalGearCharacter{
    @OverridevoiduseWeapon(String weapon){
        System.out.println("Big Boss is using a " + weapon);
    }
   voidgiveOrderToTheArmy(String orderMessage){
        System.out.println(orderMessage);
    }
}
publicclassSolidSnakeextendsMetalGearCharacter{
    voiduseWeapon(String weapon){
        System.out.println("Solid Snake is using a " + weapon);
    }
}
publicclassUseSpecificMethod{
    publicstaticvoidexecuteActionWith(MetalGearCharacter metalGearCharacter){
        metalGearCharacter.useWeapon("SOCOM");
        // Следующая строка не будет работать// metalGearCharacter.giveOrderToTheArmy("Attack!");if (metalGearCharacter instanceof BigBoss) {
            ((BigBoss) metalGearCharacter).giveOrderToTheArmy("Attack!");
        }
    }
publicstaticvoidmain(String... specificPolymorphismInvocation){
        executeActionWith(new SolidSnake());
        executeActionWith(new BigBoss());
    }
}

The technique we use here is casting or consciously changing the type of an object at run time.


Note that calling a specific method is possible only when casting a more general type to a more specific type. A good analogy would be to tell the compiler explicitly: "Hey, I know what I'm doing here, so I'm going to cast the object to a specific type and will use this method."


Referring to the above example, the compiler has a good reason for not accepting calls to certain methods: the class that is passed must be SolidSnake. In this case, the compiler has no way to ensure that each subclass MetalGearCharacterhas a method giveOrderToTheArmy.


Keyword instanceof


Pay attention to the reserved word instanceof. Before calling a particular method, we asked if it was an MetalGearCharacterinstanceof BigBoss. If it is not an instance BigBoss, we will get the following exception:


Exception in thread `main" java.lang.ClassCastException: 
com.javaworld.javachallengers.polymorphism.specificinvocation.SolidSnake cannot 
be cast to com.javaworld.javachallengers.polymorphism.specificinvocation.BigBoss

Keyword super


What if we want to refer to an attribute or method from the parent class? In this case, we can use the keyword super.
For example:


publicclassJavaMascot{
  voidexecuteAction(){
    System.out.println("The Java Mascot is about to execute an action!");
  }
}
publicclassDukeextendsJavaMascot{
  @OverridevoidexecuteAction(){
    super.executeAction();
    System.out.println("Duke is going to punch!");
  }
  publicstaticvoidmain(String... superReservedWord){
    new Duke().executeAction();
  }
}

Using a reserved word superin a executeActionclass method Dukecalls the parent class method. Then we perform a specific action from the class Duke. That is why we can see both messages in the output:


The Java Mascot is about to execute an action!
Duke is going to punch!

Solve the problem of polymorphism


Let's check what you learned about polymorphism and inheritance.


In this problem you are given several methods from Matt Groening's The Simpsons, Vavam is required to unravel what the output for each class will be. First, carefully review the following code:


publicclassPolymorphismChallenge{
    staticabstractclassSimpson{
        voidtalk(){
            System.out.println("Simpson!");
        }
        protectedvoidprank(String prank){
            System.out.println(prank);
        }
    }
    staticclassBartextendsSimpson{
        String prank;
        Bart(String prank) { this.prank = prank; }
        protectedvoidtalk(){
            System.out.println("Eat my shorts!");
        }
        protectedvoidprank(){
            super.prank(prank);
            System.out.println("Knock Homer down");
        }
    }
    staticclassLisaextendsSimpson{
        voidtalk(String toMe){
            System.out.println("I love Sax!");
        }
    }
    publicstaticvoidmain(String... doYourBest){
        new Lisa().talk("Sax :)");
        Simpson simpson = new Bart("D'oh");
        simpson.talk();
        Lisa lisa = new Lisa();
        lisa.talk();
        ((Bart) simpson).prank();
    }
}

What do you think? What will be the result? Do not use IDE to find out! The goal is to improve your code analysis skills, so try to solve it yourself.


Choose your answer (the correct answer can be found at the end of the article).


A)
I love Sax!
D'oh
Simpson!
D'oh


B)
Sax :)
Eat my shorts!
I love Sax!
D'oh
Knock Homer down


C)
Sax :)
D'oh
Simpson!
Knock homer down


D)
I love Sax!
Eat my shorts!
Simpson!
D'oh
Knock Homer down


What happened? Understanding polymorphism


For the next method call:


new Lisa().talk("Sax :)");

The output will be "I love Sax!". This is because we are passing a string to a method, and the class Lisahas such a method.


For the next call:


Simpson simpson = new Bart("D'oh");
simpson.talk();

The output will be "Eat my shorts!". This is because we initialize the type Simpsonwith Bart.


Now look, this is a bit more complicated:


Lisa lisa = new Lisa();
lisa.talk();

Here we use method overloading with inheritance. We do not pass anything to the method talk, so the method talkfrom is Simpsoncalled.


In this case, the output will be "Simpson!".


Here is another one:


((Bart) simpson).prank();

In this case, the string prankwas passed when creating an instance of the class Bartvia new Bart("D'oh");. In this case, the method is first called super.prank()and then the method prank()from the class Bart. The output will be:


"D'oh""Knock Homer down"

Common mistakes with polymorphism


A common mistake is to think that you can call a particular method without using a cast.


Another mistake is uncertainty about which method will be called upon polymorphic creation of a class instance. Remember that the method being called is a method of the created instance.


Also remember that overriding a method is not a method overload.


Cannot override a method if the parameters differ. You can change the return type of the overridden method if the return type is a subclass.


What you need to remember about polymorphism


  • The created instance determines which method will be invoked using polymorphism.


  • The annotation @Overrideobliges the programmer to use the overridden method; otherwise, a compiler error will occur.


  • Polymorphism can be used with ordinary classes, abstract classes and interfaces.


  • Most design patterns are dependent on some form of polymorphism.


  • The only way to call your desired method in a polymorphic subclass is to use a type conversion.


  • You can create a powerful code structure using polymorphism.


  • Experiment. Through this, you will be able to master this powerful concept!



Answer


The answer is D.


The output will be:


I love Sax!
Eat my shorts! 
Simpson!
D'oh
Knock Homer down

As always, I welcome your comments and questions. And we are waiting for Vitali in the open lesson .


Also popular now: