JavaRush /Java Blog /Random EN /Polymorphism in Java

Polymorphism in Java

Published in the Random EN group
Questions about OOP are an integral part of a technical interview for the position of a Java developer in an IT company. In this article we will talk about one of the principles of OOP - polymorphism. We will focus on aspects that are often asked about during interviews, and also provide small examples for clarity.

What is polymorphism?

Polymorphism is the ability of a program to identically use objects with the same interface without information about the specific type of this object. If you answer the question what is polymorphism in this way, you will most likely be asked to explain what you mean. Once again, without asking for a bunch of additional questions, put everything in order for the interviewer.

Polymorphism in Java at an interview - 1
We can start with the fact that the OOP approach involves building a Java program based on the interaction of objects that are based on classes. Classes are pre-written drawings (templates) according to which objects in the program will be created. Moreover, a class always has a specific type, which, with a good programming style, “tells” its purpose by its name. Further, it can be noted that since Java is a strongly typed language, the program code always needs to indicate the type of the object when declaring variables. Add to this that strict typing increases code safety and program reliability and allows you to prevent type incompatibility errors (for example, an attempt to divide a string by a number) at the compilation stage. Naturally, the compiler must “know” the declared type - this can be a class from the JDK or one we created ourselves. Please note to the interviewer that when working with program code, we can use not only objects of the type that we assigned when declaring, but also its descendants. This is an important point: we can treat many types as if they were just one (as long as those types are derived from a base type). This also means that, having declared a variable of a superclass type, we can assign it the value of one of its descendants. The interviewer will like it if you give an example. Select some object that can be common (base) for a group of objects and inherit a couple of classes from 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 "Я " + name + ", to me " + age + " years. " ;
    }
}
In the descendants, override the base class method:
public class ElectricBoogieDancer extends Dancer {
    public ElectricBoogieDancer(String name, int age) {
        super(name, age);
    }
// overriding the base class method
    @Override
    public void dance() {
        System.out.println( toString() + "I dance electric boogie!");
    }
}

public class BreakDankDancer extends Dancer{

    public BreakDankDancer(String name, int age) {
        super(name, age);
    }
// overriding the base class method
    @Override
    public void dance(){
        System.out.println(toString() + "I'm breakdancing!");
    }
}
An example of polymorphism in Java and the use of objects in a program:
public class Main {

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

        Dancer breakDanceDancer = new BreakDankDancer("Alexei", 19);// upcast to base type
        Dancer electricBoogieDancer = new ElectricBoogieDancer("Igor", 20); // upcast to base type

        List<Dancer> discotheque = Arrays.asList(dancer, breakDanceDancer, electricBoogieDancer);
        for (Dancer d : discotheque) {
            d.dance();// polymorphic method call
        }
    }
}
mainShow in the method code what is in the lines:
Dancer breakDanceDancer = new BreakDankDancer("Alexei", 19);
Dancer electricBoogieDancer = new ElectricBoogieDancer("Igor", 20);
We declared a superclass type variable and assigned it the value of one of the descendants. Most likely, you will be asked why the compiler will not complain about the mismatch between the types declared to the left and to the right of the assignment sign, since Java has strict typing. Explain that upward type conversion works here - a reference to an object is interpreted as a reference to the base class. Moreover, the compiler, having encountered such a construction in the code, does this automatically and implicitly. Based on the example code, it can be shown that the class type declared to the left of the assignment sign Dancerhas several forms (types) declared to the right BreakDankDancer, ElectricBoogieDancer. Each of the forms can have its own unique behavior for common functionality defined in the superclass - method dance. That is, a method declared in a superclass can be implemented differently in its descendants. In this case, we are dealing with method overriding, and this is exactly what creates a variety of forms (behaviors). You can see this by running the main method code for execution: Program output I'm Anton, I'm 18 years old. I dance like everyone else. I'm Alexey, I'm 19 years old. I breakdance! I'm Igor, I'm 20 years old. I dance the electric boogie! If we do not use overriding in the descendants, then we will not get different behavior. BreakDankDancerFor example, if we ElectricBoogieDancercomment out the method for our classes dance, the output of the program will be like this: I’m Anton, I’m 18 years old. I dance like everyone else. I'm Alexey, I'm 19 years old. I dance like everyone else. I'm Igor, I'm 20 years old. I dance like everyone else. and this means that there is simply no point in creating new BreakDankDancerclasses ElectricBoogieDancer. What, exactly, is the principle of Java polymorphism? Where is it hidden to use an object in a program without knowing its specific type? In our example, this is a method call d.dance()on an object dof type Dancer. Java polymorphism means that the program does not need to know what type the object BreakDankDanceror object will be ElectricBoogieDancer. The main thing is that it is a descendant of the class Dancer. And if we talk about descendants, it should be noted that inheritance in Java is not only extends, but also implements. Now is the time to remember that Java does not support multiple inheritance - each type can have one parent (superclass) and an unlimited number of descendants (subclasses). Therefore, interfaces are used to add multiple functionality to classes. Interfaces reduce the coupling of objects to a parent compared to inheritance and are used very widely. In Java, an interface is a reference type, so a program can declare the type to be a variable of the interface type. This is a good time to give an example. Let's create the interface:
public interface Swim {
    void swim();
}
For clarity, let’s take different and unrelated objects and implement an interface in them:
public class Human implements Swim {
    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 inflatable ring.");
    }

    @Override
    public String toString() {
        return "Я " + name + ", to me " + age + " years. ";
    }

}

public class Fish implements Swim{
    private String name;

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

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

    }

public class UBoat implements Swim {

    private int speed;

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

    @Override
    public void swim() {
        System.out.println("The submarine is sailing, rotating the propellers, at a speed" + speed + " knots.");
    }
}
Method main:
public class Main {

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

        List<Swim> swimmers = Arrays.asList(human, fish, boat);
        for (Swim s : swimmers) {
            s.swim();
        }
    }
}
The result of executing a polymorphic method defined in an interface allows us to see differences in the behavior of the types that implement that interface. They consist in different results of method execution swim. After studying our example, the interviewer may ask why, when executing the code frommain
for (Swim s : swimmers) {
            s.swim();
}
Are the methods defined in these classes called for our objects? How do you select the desired implementation of a method when executing a program? To answer these questions, we need to talk about late (dynamic) binding. By binding we mean establishing a connection between a method call and its specific implementation in classes. Essentially, the code determines which of the three methods defined in the classes will be executed. Java by default uses late binding (at runtime rather than at compile time, as is the case with early binding). This means that when compiling the code
for (Swim s : swimmers) {
            s.swim();
}
the compiler does not yet know which class the code is from Human, Fishor whether Uboatit will be executed in the swim. This will be determined only when the program is executed thanks to the mechanism of dynamic dispatch - checking the type of the object during program execution and selecting the desired implementation of the method 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 in them associates variables with their values, and objects with their methods. Moreover, if an object is inherited or implements an interface, the presence of overridden methods in its class is first checked. If there are any, they are tied to this type, if not, a method is searched that is defined in the class one level higher (in the parent) and so on up to the root in a multi-level hierarchy. Speaking about polymorphism in OOP and its implementation in program code, we note that it is good practice to use abstract descriptions to define base classes using abstract classes as well as interfaces. This practice is based on the use of abstraction - isolating common behavior and properties and enclosing them within an abstract class, or isolating only the common behavior - in which case we create an interface. Building and designing a hierarchy of objects based on interfaces and class inheritance is a prerequisite for fulfilling the principle of OOP polymorphism. Regarding the issue of polymorphism and innovations in Java, we can mention that when creating abstract classes and interfaces, starting with Java 8, it is possible to write a default implementation of abstract methods in base classes using the keyword default. For example:
public interface Swim {
    default void swim() {
        System.out.println("Just floating");
    }
}
Sometimes they may ask about the requirements for declaring methods in base classes so that the principle of polymorphism is not violated. Everything is simple here: these methods should not be static , private and final . Private makes the method available only in the class, and you cannot override it in the descendant. Static makes the method the property of the class, not the object, so the superclass method will always be called. Final will make the method immutable and hidden from its heirs.

What does polymorphism give us in Java?

The question of what the use of polymorphism gives us will most likely also arise. Here you can answer briefly, without going too deep into the weeds:
  1. Allows you to replace object implementations. This is what testing is based on.
  2. Provides program expandability - it becomes much easier to create a foundation for the future. Adding new types based on existing ones is the most common way to expand the functionality of programs written in OOP style.
  3. Allows you to combine objects with a common type or behavior into one collection or array and manipulate them uniformly (as in our examples, making everyone dance - a method danceor swim - a method swim).
  4. Flexibility when creating new types: you can choose to implement a method from a parent or override it in a child.

Parting words for the journey

The principle of polymorphism is a very important and broad topic. It covers almost half of Java's OOP and a good portion of the language's basics. You won’t be able to get away with defining this principle in an interview. Ignorance or misunderstanding of it will most likely put an end to the interview. Therefore, do not be lazy to check your knowledge before the test and refresh it if necessary.
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION