Skip to content
On this page

Polymorphism

In an inheritance relationship, if a subclass defines a method with exactly the same signature as the parent class method, it is called an override.

For example, in the Person class, we define run() method:

java
class Person {
  public void run() {
    System.out.println("Person.run");
  }
}

In the subclass Student , override this run() method:

java
class Student extends Person {
    @Override
    public void run() {
        System.out.println("Student.run");
    }
}

The difference between Override and Overload is that if the method signature is different, it is Overload, and the Overload method is a new method; if the method signature is the same and the return value is the same, it is Override .

Notice

The method names are the same, the method parameters are the same, but the method return values are different, and they are also different methods. In a Java program, if this happens, the compiler will report an error.

java
class Person {
    public void run() { … }
}

class Student extends Person {
    // Not Override, because the parameters are different:
    public void run(String s) { … }
    // Not Override, because the return value is different:
    public int run() { … }
}

Adding @Override allows the compiler to help check whether the correct override has been performed. I hope to override, but accidentally write the wrong method signature, and the compiler will report an error.

java
public class Main {
    public static void main(String[] args) {
    }
}

class Person {
    public void run() {}
}

public class Student extends Person {
    @Override // Compile error!
    public void run(String s) {}
}

But @Override is not required.

In the previous section, we already knew that the declared type of a reference variable may not match its actual type, for example:

java
Person p = new Student();

Now, let's consider a situation if the subclass overrides the parent class's method:

java
public class Main {
    public static void main(String[] args) {
        Person p = new Student();
        p.run(); // Should it print Person.run or Student.run?
    }
}

class Person {
    public void run() {
        System.out.println("Person.run");
    }
}

class Student extends Person {
    @Override
    public void run() {
        System.out.println("Student.run");
    }
}

So, if a variable of actual type is Student and reference type is Person , and its run() method is called, is the run() method of Person or Student called?

If you run the above code, you will know that the method actually called is Student 's run() method. Therefore it can be concluded that:

Java's instance method calls are dynamic calls based on the actual type at runtime, not the declared type of the variable.

This very important feature is called polymorphism in object-oriented programming. Its English spelling is very complicated: Polymorphic.

Polymorphism

Polymorphism means that for a method call of a certain type, the actual method executed depends on the actual type of method at runtime. For example:

java
Person p = new Student();
p.run(); // Unable to determine which run() method is called at runtime

Some students will say that it is clear from the above code that the run() method of Student must be called.

However, suppose we write a method like this:

java
public void runTwice(Person p) {
    p.run();
    p.run();
}

The parameter type it passes in is Person . We have no way of knowing whether the actual type of the parameter passed in is Person , Student , or other subclasses of Person such as Teacher . Therefore, we cannot determine whether the call is run(defined by the Person class run() method.

Therefore, the characteristic of polymorphism is that the subclass method to be called can be dynamically determined during the runtime. When a method is called on a certain type, the actual method executed may be an overridden method of a subclass. What exactly does this uncertain method call do?

Let's give an example.

Suppose we define a kind of income and need to file taxes on it, then first define an Income class:

java
class Income {
    protected double income;
    public double getTax() {
        return income * 0.1; // Tax rate 10%
    }
}

For salary income, a base can be subtracted, then we can derive SalaryIncome from Income and override getTax() :

java
class Salary extends Income {
    @Override
    public double getTax() {
        if (income <= 5000) {
            return 0;
        }
        return (income - 5000) * 0.2;
    }
}

If you enjoy special allowances from the State Council, then according to regulations, you can be fully tax-free:

java
class StateCouncilSpecialAllowance extends Income {
    @Override
    public double getTax() {
        return 0;
    }
}

Now, we need to write a financial software for tax reporting. To report taxes on all a person's income, we can write like this:

java
public double totalTax(Income... incomes) {
    double total = 0;
    for (Income income: incomes) {
        total = total + income.getTax();
    }
    return total;
}

Let’s try it:

java
public class Main {
    public static void main(String[] args) {
        // Calculate taxes for a friend who has ordinary income, 
        // salary income and enjoys special allowances from the State Council:
        Income[] incomes = new Income[] {
            new Income(3000),
            new Salary(7500),
            new StateCouncilSpecialAllowance(15000)
        };
        System.out.println(totalTax(incomes));
    }

    public static double totalTax(Income... incomes) {
        double total = 0;
        for (Income income: incomes) {
            total = total + income.getTax();
        }
        return total;
    }
}

class Income {
    protected double income;

    public Income(double income) {
        this.income = income;
    }

    public double getTax() {
        return income * 0.1; // 税率10%
    }
}

class Salary extends Income {
    public Salary(double income) {
        super(income);
    }

    @Override
    public double getTax() {
        if (income <= 5000) {
            return 0;
        }
        return (income - 5000) * 0.2;
    }
}

class StateCouncilSpecialAllowance extends Income {
    public StateCouncilSpecialAllowance(double income) {
        super(income);
    }

    @Override
    public double getTax() {
        return 0;
    }
}

Observe totalTax() method: Using polymorphism, totalTax() method only needs to deal with Income . It does not need to know the existence of Salary and StateCouncilSpecialAllowance at all to correctly calculate the total tax. If we want to add a new type of royalty income, we only need to derive it from Income and then overwrite getTax() method correctly. Pass the new type into totalTax() without modifying any code.

It can be seen that polymorphism has a very powerful function, which allows adding more types of subclasses to implement functional expansion without modifying the code based on the parent class.

Override Object Methods

Because all class ultimately inherit from Object , and Object defines several important methods:

  • toString() : Output instance as String ;
  • equals() : Determine whether two instances are logically equal;
  • hashCode() : Calculate the hash value of an instance.

If necessary, we can override these methods of Object . For example:

java
class Person {
    ...
    @Override
    public String toString() {
        return "Person:name=" + name;
    }

    // Compare for equality:
    @Override
    public boolean equals(Object o) {
        // if and only if o is of type Person:
        if (o instanceof Person) {
            Person p = (Person) o;
            // And when the name fields are the same, return true:
            return this.name.equals(p.name);
        }
        return false;
    }

    // Calculate hash:
    @Override
    public int hashCode() {
        return this.name.hashCode();
    }
}

call super

In the overridden method of the subclass, if you want to call the overridden method of the parent class, you can call it through super . For example:

java
class Person {
    protected String name;
    public String hello() {
        return "Hello, " + name;
    }
}

class Student extends Person {
    @Override
    public String hello() {
        // Call the hello() method of the parent class:
        return super.hello() + "!";
    }
}

final

Inheritance allows subclasses to override parent class methods. If a parent class does not allow subclasses to override one of its methods, it can mark the method as final . Methods modified with final cannot be Override :

java
class Person {
    protected String name;
    public final String hello() {
        return "Hello, " + name;
    }
}

class Student extends Person {
    // compile error: Overwriting is not allowed
    @Override
    public String hello() {
    }
}

If a class does not want any other classes to inherit from it, it can mark the class itself as final . Classes modified with final cannot be inherited:

java
final class Person {
    protected String name;
}

// compile error: Not allowed to inherit from Person
class Student extends Person {
}

For instance fields of a class, you can also use final modification. Fields modified with final cannot be modified after initialization. For example:

java
class Person {
    public final String name = "Unamed";
}

Reassigning a final field will result in an error:

java
Person p = new Person();
p.name = "New Name"; // compile error!

Final fields can be initialized in the constructor:

java
class Person {
    public final String name;
    public Person(String name) {
        this.name = name;
    }
}

This method is more commonly used because it ensures that once the instance is created, its final fields cannot be modified.

Summary

Subclasses can override parent class methods (Override), and override changes the behavior of parent class methods in the subclass;

Java's method calls always act on the actual type of the runtime object. This behavior is called polymorphism;

The final modifier has multiple functions:

  • Methods decorated final can prevent them from being overwritten;
  • Classes modified with final can prevent inheritance;
  • final modified fields must be initialized when the object is created and cannot be modified subsequently.
Polymorphism has loaded