rafael_del nero
Java Developer

Polymorphism and inheritance in Java

how-to
Jun 13, 202410 mins
JavaProgramming LanguagesSoftware Development

Get started with polymorphism in Java and how to do method invocation in polymorphic method calls.

Polymorphism—or an object’s ability to execute specialized actions based on its type—is what makes Java code flexible. Many design patterns created by the Gang Of Four rely on some form of polymorphism, including the Command pattern. In this article, you will learn the basics of Java polymorphism and how to use it in your programs.

Things to know about Java polymorphism

  • Polymorphism and Java inheritance
  • Why polymorphism is important
  • Polymorphism in method overriding
  • Polymorphism with the core Java classes
  • Polymorphic method calls and casting
  • Reserved keywords and polymorphism
  • Common mistakes with polymorphism
  • What to remember about polymorphism

Polymorphism and Java inheritance

We will focus on the relationship between polymorphism and Java inheritance. The main thing to keep in mind is that polymorphism requires an inheritance or an interface implementation. You can see this in the example below, featuring Duke and 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();
    }
}

The output from this code will be:

 
Punch!
Fly!

Because of their specific implementations, both Duke‘s and Juggy’s actions will be executed.

Why polymorphism is important

The purpose of using polymorphism is to decouple the client class from the implementation code. Instead of being hard-coded, the client class receives the implementation to execute the necessary action. In this way, the client class knows just enough to execute its actions, which is an example of loose coupling.

To better understand the advantages of polymorphism,  take a look at the 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();
    }
}

In this example, you can see that the SweetCreator class only knows the SweetProducer class. It doesn’t know the implementation of each Sweet. That separation gives us the flexibility to update and reuse our classes, and it makes the code much easier to maintain. When designing your code, always look for ways to make it as flexible and maintainable as possible. Polymorphism is a very powerful technique for writing reusable Java code.

Tip: The @Override annotation obligates the programmer to use the same method signature that must be overridden. If the method is not overridden, there will be a compilation error.

Is method overloading polymorphism?

Many programmers are confused about the relationship of polymorphism to method overriding and method overloading. Only method overriding is true polymorphism, however. Overloading shares the same method’s name but the parameters are different. Polymorphism is a broad term, so there will always be discussions about this topic,

Polymorphism in method overriding

It’s possible to change the return type of an overridden method if it is a covariant type. A covariant type is essentially a subclass of the return type. Here’s an example:


public abstract class JavaMascot {
    abstract JavaMascot getMascot();
}
public class Duke extends JavaMascot {
    @Override
    Duke getMascot() {
        return new Duke();
    }
}

Because Duke is a JavaMascot, we can change the return type when overriding.

Polymorphism with the core Java classes

We use polymorphism all the time in the core Java classes. One very simple example is when we instantiate the ArrayList class declaring the  List interface as a type:


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

To go further, consider this code sample using the Java Collections API without polymorphism:


public class ListActionWithoutPolymorphism {
   // Example without polymorphism
    void executeVectorActions(Vector<Object> vector) {/* Code repetition here*/}
    void executeArrayListActions(ArrayList<Object> arrayList) {/*Code repetition here*/}
    void executeLinkedListActions(LinkedList<Object> linkedList) {/* Code repetition here*/}
    void executeCopyOnWriteArrayListActions(CopyOnWriteArrayList<Object> copyOnWriteArrayList)
        { /* Code repetition here*/}
}
public class ListActionInvokerWithoutPolymorphism {
        listAction.executeVectorActions(new Vector<>());
        listAction.executeArrayListActions(new ArrayList<>());
        listAction.executeLinkedListActions(new LinkedList<>());
        listAction.executeCopyOnWriteArrayListActions(new CopyOnWriteArrayList<>());
}

That’s ugly code, isn’t it? Imagine trying to maintain it! Now look at the same example with polymorphism:


public static void main(String … polymorphism) {
ListAction listAction = new ListAction();	
	listAction.executeListActions();
}
public class ListAction {
    void executeListActions(List<Object> list) {
        // Execute actions with different lists
    }
}
public class ListActionInvoker {
    public static void main(String... masterPolymorphism) {
        ListAction listAction = new ListAction();
        listAction.executeListActions(new Vector<>());
        listAction.executeListActions(new ArrayList<>());
        listAction.executeListActions(new LinkedList<>());
        listAction.executeListActions(new CopyOnWriteArrayList<>());
    }
}

The benefits of polymorphism are flexibility and extensibility. Instead of creating several different methods, we can declare just one method that receives the generic List type.

Polymorphic method calls and casting

It’s possible to invoke specific methods in a polymorphic call, but doing it comes at the cost of flexibility. Here’s an example:


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");
	// The below line wouldn't work
        // metalGearCharacter.giveOrderToTheArmy("Attack!");
        if (metalGearCharacter instanceof BigBoss) {
            ((BigBoss) metalGearCharacter).giveOrderToTheArmy("Attack!");
        }
    }
public static void main(String... specificPolymorphismInvocation) {
        executeActionWith(new SolidSnake());
        executeActionWith(new BigBoss());
    }
}

The technique we’re using here is casting, or deliberately changing the object type at runtime.

Note that it’s possible to invoke a specific method only when casting the generic type to the specific type. A good analogy would be saying explicitly to the compiler, “Hey, I know what I’m doing here, so I’m going to cast the object to a specific type and use a specific method.”

Referring to the above example, there is an important reason the compiler refuses to accept specific method invocation: the class that is being passed could be SolidSnake. In this case, there is no way for the compiler to ensure every subclass of MetalGearCharacter has the giveOrderToTheArmy method declared.

Get the code: Get the source code for this challenge and run your own tests.

Reserved keywords

Pay attention to the reserved word instanceof. Before invoking the specific method we’ve asked if MetalGearCharacter is “instanceofBigBoss. If it wasn’t a BigBoss instance, we would receive the following exception message:


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

What if we wanted to reference an attribute or method from a Java superclass? In this case, we could use the super reserved word. For example:


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();
  }
}

Using the reserved word super in Duke’s executeAction method invokes the superclass method. We then execute the specific action from Duke. That’s why we can see both messages in the output below:


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

Common mistakes with polymorphism

  • It’s a common mistake to think it’s possible to invoke a specific method without using casting.
  • Another mistake is being unsure what method will be invoked when instantiating a class polymorphically. Remember that the method to be invoked is the method of the created instance.
  • Also remember that method overriding is not method overloading.
  • It’s impossible to override a method if the parameters are different. It is possible to change the return type of the overridden method if the return type is a subclass of the superclass method.

What to remember about polymorphism

  • The created instance will determine what method will be invoked when using polymorphism.
  • The @Override annotation obligates the programmer to use an overridden method; if not, there will be a compiler error.
  • Polymorphism can be used with normal classes, abstract classes, and interfaces.
  • Most design patterns depend on some form of polymorphism.
  • The only way to use a specific method in your polymorphic subclass is by using casting.
  • It’s possible to design a powerful structure in your code using polymorphism.

Take the Java polymorphism challenge!

Let’s try out what you’ve learned about polymorphism and inheritance. In this challenge, you’re given a handful of methods from Matt Groening’s The Simpsons, and your challenge is to deduce what the output for each class will be. To start, analyze the following code carefully:


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();
    }
}

What do you think? What will the final output be? Don’t use an IDE to figure this out! The point is to improve your code analysis skills, so try to determine the output for yourself.

Choose your answer and you’ll be able to find the correct answer below.

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

Solving the challenge

For the following method invocation:


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

the output will be “I love Sax!” This is  because we are passing a String to the method and Lisa has the method.

For the next invocation:


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

The output will be “Eat my shorts!” This is because we’re instantiating  the Simpson type with Bart.

Now check this one, which is a little trickier:


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

Here, we are using method overloading with inheritance. We are not passing anything to the talk method, which is why the Simpson talk method is invoked.  In this case, the output will be:


"Simpson!"

Here’s one more:


((Bart) simpson).prank();

In this case, the prank String was passed when we instantiated the Bart class with new Bart("D'oh");. In this case,  first the super.prank method will be invoked, followed by the specific prank method from Bart. The output will be:


"D'oh"
"Knock Homer down"

The answer to this Java challenger, therefore, is D. The output would be:


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

Video challenge! Debugging Java polymorphism and inheritance

Debugging is one of the easiest ways to fully absorb programming concepts while also improving your code. In this video, you can follow along while Rafael debugs and explains the Java polymorphism challenge:

Learn more about Java

rafael_del nero
Java Developer

Rafael del Nero is a Java Champion and Oracle Ace, creator of the Java Challengers initiative, and a quiz master in the Oracle Dev Gym. Rafael is the author of "Java Challengers" and "Golden Lessons." He believes there are many techniques involved in creating high-quality software that developers are often unaware of. His purpose is to help Java developers use better programming practices to code quality software for stress-free projects with fewer bugs.

More from this author