OOP-related questions are an integral part of the technical interview for a Java developer position in an IT company. In this article, we will talk about one principle of OOP – polymorphism. We'll focus on the aspects that are often asked about during interviews, and also give a few examples for clarity.

What is polymorphism in Java?

Polymorphism is a program's ability to treat objects with the same interface in the same way, without information about the object's specific type. If you answer a question about what polymorphism is, you'll most likely be asked to explain what you meant. Without triggering a bunch of additional questions, lay it all out for the interviewer once again. Interview time: polymorphism in Java - 1You can start with the fact that the OOP approach involves building a Java program based on the interaction between objects, which are based on classes. Classes are previously written blueprints (templates) used to create objects in the program. Moreover, a class always has a specific type, which, with good programming style, has a name that suggests its purpose. Further, it can be noted that since Java is strongly typed, the program code must always specify an object type when variables are declared. Add to this the fact that strict typing improves the code security and reliability, and makes it possible, even at compilation, to prevent errors due to incompatibility types (for example, trying to divide a string by a number). Naturally, the compiler must "know" the declared type – it can be a class from the JDK or one that we created ourselves. Point out to the interviewer that our code can use not only the objects of the type indicated in the declaration but also its descendants. This is an important point: we can work with many different types as a single type (provided that these types are derived from a base type). This also means that if we declare a variable whose type is a superclass, then we can assign an instance of one of its descendants to that variable. The interviewer will like it if you give an example. Select some class that could be shared by (a base class for) several classes and make a couple of them inherit it. Base class:

public class Dancer {
    private String name;
    private int age;

    public Dancer(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public void dance() {
        System.out.println(toString() + " I dance like everyone else.");
    }

    @Override
    public String toString() {
        Return "I'm " + name + ". I'm " + age + " years old.";
    }
}
In the subclasses, override the method of the base class:

public class ElectricBoogieDancer extends Dancer {
    public ElectricBoogieDancer(String name, int age) {
        super(name, age);
    }
// Override the method of the base class
    @Override
    public void dance() {
        System.out.println(toString () + " I dance the electric boogie!");
    }
}

public class Breakdancer extends Dancer {

    public Breakdancer(String name, int age) {
        super(name, age);
    }
// Override the method of the base class
    @Override
    public void dance() {
        System.out.println(toString() + " I breakdance!");
    }
}
An example of polymorphism and how these objects might be used in a program:

public class Main {

    public static void main(String[] args) {
        Dancer dancer = new Dancer("Fred", 18);

        Dancer breakdancer = new Breakdancer("Jay", 19); // Widening conversion to the base type 
        Dancer electricBoogieDancer = new ElectricBoogieDancer("Marcia", 20); // Widening conversion to the base type

        List<dancer> disco = Arrays.asList(dancer, breakdancer, electricBoogieDancer);
        for (Dancer d : disco) {
            d.dance(); // Call the polymorphic method
        }
    }
}
In the main method, show that the lines

Dancer breakdancer = new Breakdancer("Jay", 19);
Dancer electricBoogieDancer = new ElectricBoogieDancer("Marcia", 20);
declare a variable of a superclass and assign it an object that is an instance of one of its descendants. You'll most likely be asked why the compiler doesn't flip out at the inconsistency of the types declared on the left and right sides of the assignment operator — after all, Java is strongly typed. Explain that a widening type conversion is at work here — a reference to an object is treated like a reference to its base class. What's more, having encountered such a construct in the code, the compiler performs the conversion automatically and implicitly. The sample code shows that the type declared on the left side of the assignment operator (Dancer) has multiple forms (types), which are declared on the right side (Breakdancer, ElectricBoogieDancer). Each form can have its own unique behavior with respect to the general functionality defined in the superclass (the dance method). That is, a method declared in a superclass may be implemented differently in its descendants. In this case, we are dealing with method overriding, which is exactly what creates multiple forms (behaviors). This can be seen by running the code in the main method: Program output: I'm Fred. I'm 18 years old. I dance like everyone else. I'm Jay. I'm 19 years old. I breakdance! I'm Marcia. I'm 20 years old. I dance the electric boogie! If we don't override the method in the subclasses, then we won't get different behavior. For example, if we comment out the dance method in our Breakdancer and ElectricBoogieDancer classes, then the output of the program will be this: I'm Fred. I'm 18 years old. I dance like everyone else. I'm Jay. I'm 19 years old. I dance like everyone else. I'm Marcia. I'm 20 years old. I dance like everyone else. And this means that it simply doesn't make sense to create the Breakdancer and ElectricBoogieDancer classes. Where specifically is the principle of polymorphism manifest? Where is an object used in the program without knowledge of its specific type? In our example, it happens when the dance() method is called on the Dancer d object. In Java, polymorphism means that the program doesn't need to know whether the object is a Breakdancer or ElectricBoogieDancer. The important thing is that it is a descendant of the Dancer class. And if you mention descendants, you should note that inheritance in Java is not just extends, but also implements. Now is the time to mention that Java does not support multiple inheritance — each type can have one parent (superclass) and an unlimited number of descendants (subclasses). Accordingly, interfaces are used to add multiple sets of functions to classes. Compared to subclasses (inheritance), interfaces are less coupled with the parent class. They are used very widely. In Java, an interface is a reference type, so the program can declare a variable of the interface type. Now it's time to give an example. Create an interface:

public interface CanSwim {
    void swim();
}
For clarity, we'll take various unrelated classes and make them implement the interface:

public class Human implements CanSwim {
    private String name;
    private int age;

    public Human(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public void swim() {
        System.out.println(toString()+" I swim with an inflated tube.");
    }

    @Override
    public String toString() {
        return "I'm " + name + ". I'm " + age + " years old.";
    }

}
 
public class Fish implements CanSwim {
    private String name;

    public Fish(String name) {
        this.name = name;
    }

    @Override
    public void swim() {
        System.out.println("I'm a fish. My name is " + name + ". I swim by moving my fins.");

    }

public class UBoat implements CanSwim {

    private int speed;

    public UBoat(int speed) {
        this.speed = speed;
    }

    @Override
    public void swim() {
        System.out.println("I'm a submarine that swims through the water by rotating screw propellers. My speed is " + speed + " knots.");
    }
}
main method:

public class Main {

    public static void main(String[] args) {
        CanSwim human = new Human("John", 6);
        CanSwim fish = new Fish("Whale");
        CanSwim boat = new UBoat(25);

        List<swim> swimmers = Arrays.asList(human, fish, boat);
        for (Swim s : swimmers) {
            s.swim();
        }
    }
}
The results calling a polymorphic method defined in an interface show us the differences in the behavior of the types that implement this interface. In our case, these are the different strings displayed by the swim method. After studying our example, the interviewer may ask why running this code in the main method

for (Swim s : swimmers) {
            s.swim();        
}
causes the overriding methods defined in our subclasses to be called? How is the method's desired implementation selected while program is running? To answer these questions, you need to explain late (dynamic) binding. Binding means establishing a mapping between a method call and its specific class implementation. In essence, the code determines which of the three methods defined in the classes will be executed. Java uses late binding by default, i.e. binding happens at runtime and not at compile time as is the case with early binding. This means that when the compiler compiles this code

for (Swim s : swimmers) {
            s.swim();        
}
it does not know which class (Human, Fish, or Uboat) has the code that will be executed when the swim method is called. This is determined only when the program is executed, thanks to the dynamic binding mechanism (checking an object's type at runtime and selecting the correct implementation for this type). If you are asked how this is implemented, you can answer that when loading and initializing objects, the JVM builds tables in memory and links variables with their values and objects with their methods. In doing so, if a class is inherited or implements an interface, the first order of business is to check for the presence of overridden methods. If there are any, they are bound to this type. If not, the search for a matching method moves to the class that is one step higher (the parent) and so on up to the root in a multilevel hierarchy. When it comes to polymorphism in OOP and its implementation in code, we note that it is good practice to use abstract classes and interfaces to provide abstract definitions of base classes. This practice follows from the principle of abstraction — identifying common behavior and properties and putting them in an abstract class, or identifying only common behavior and putting it in an interface. Designing and creating an object hierarchy based on interfaces and class inheritance are required to implement polymorphism. Regarding polymorphism and innovations in Java, we note that starting with Java 8, when creating abstract classes and interfaces it is possible to use the default keyword to write a default implementation for abstract methods in base classes. For example:

public interface CanSwim {
    default void swim() {
        System.out.println("I just swim");
    }
}
Sometimes interviewers ask about how methods in base classes must be declared so that the principle of polymorphism is not violated. The answer is simple: these methods must not be static, private nor final. Private makes a method available only within a class, so you won't be able to override it in a subclass. Static associates a method with the class rather than any object, so the superclass's method will always be called. And final makes a method immutable and hidden from subclasses.

What does polymorphism give us?

You'll also most likely be asked about how polymorphism benefits us. You can answer this briefly without getting bogged down in the hairy details:
  1. It makes it possible to replace class implementations. Testing is built on it.
  2. It facilitates extensibility, making it much easier to create a foundation that can be built upon in the future. Adding new types based on existing ones is the most common way to expand the functionality of OOP programs.
  3. It lets you to combine objects that share a common type or behavior into one collection or array and handle them uniformly (as in our examples, where we forced everyone to dance() or swim() :)
  4. Flexibility in creating new types: you can opt for the parent's implementation of a method or override it in a subclass.

Some parting words

Polymorphism is a very important and extensive topic. It's the subject of almost half of this article on OOP in Java and forms a good portion of the language's foundation. You won't be able to avoid defining this principle in an interview. If you don't know it or don't understand it, the interview will probably come to an end. So don't be a slacker — assess your knowledge before the interview and refresh it if necessary.

More reading: