Java Polymorphism Tutorial

In this Java tutorial we learn how to override (runtime polymorphism) and overload (compile-time polymorphism) our methods.

We also discuss the abstract modifier that forces other developers to follow our design, and the final modifier that prevents classes from being inherited.

What is Polymorphism

Another one of the three core principles in any object oriented program is polymorphism. The word polymorphism means having many forms.

As an example, consider the + operator. It has multiple forms. When used on numbers, it will add the numbers together. When used on strings, it will concatenate (join) the strings together.

Example:
public class Program {
    public static void main(String[] args) {

        int num1 = 5;
        int num2 = 3;

        // + adds numbers together
        // producing a result
        System.out.println(num1 + num2);

        String str1 = "Hello ";
        String str2 = "there";

        // + joins multiple strings
        // into a single string
        System.out.println(str1 + str2);
    }
}

In Java, we have two types of polymorphism:

  1. Compile-time Polymorphism allows methods to be overloaded.
  2. Runtime Polymorphism allows methods to be overridden.

Compile-time polymorphism in Java

Compile-time polymorphism is when we overload a method (similar to the + operator).

Overloading allows us to have multiple methods of the same name, with either different or enhanced functionality.

How to overload a method in Java

When overloading a method, we must specify different or additional parameters.

Syntax:
modifier return_type name() {
    // body
}

// same name but with
// added parameter
modifier return_type name(param) {
    // body
}
Example:
public class Program {
    public static void main(String[] args) {

        Logger log = new Logger();

        // one of the methods doesn't
        // accept any parameters so if
        // we don't pass any arguments
        // to the method, the compiler
        // will choose the first method
        log.logMessage();

        // if we do pass it a string
        // argument, the compiler will
        // choose the method with the
        // 'msg' string parameter
        log.logMessage("Hello there");
    }
}

class Logger {

    // both methods have the same
    // name, but this one doesn't
    // have any parameters
    public void logMessage() {
        System.out.println("Hello, World!");
    }

    // and this one has a single
    // string parameter
    public void logMessage(String msg) {
        System.out.println(msg);
    }
}

In the example above, we have two methods called ‘logMessage’. One has a parameter, the other doesn’t.

If we want the text “Hello, World!” to be printed to the console, we don’t pass an argument to the method. The compiler will see there’s no argument and choose version of the method without a parameter.

Runtime polymorphism in Java

Runtime polymorphism is when we override a method that exists in a parent class, with a method in a child class.

How to override a method in Java

To override a method, we create a method in the child class with exactly the same name as the one in the parent class.

We must also mark the method that will be overridden in the parent class as virtual, and the method that will override it in the child class as override.

Syntax:
class parent_class {

    modifier return_type name() {
        // default functionality
    }
}

class child_class {

    modifier return_type name() {
        // new/enhanced functionality
    }
}

// optionally a child class
// can use the @Override
// annotation to indicate a
// method overrides another
// but then strict rules
// apply and must be followed
class child_class {

    @Override
    modifier return_type name() {
        // new/enhanced functionality
    }
}
Example:
public class Program {
    public static void main(String[] args) {

        Car car = new Car();
        car.Drive();

        Coupe coupe = new Coupe();
        // same method name, but
        // the message printed is
        // different
        coupe.Drive();
    }
}

class Car {

    public void Drive() {
        System.out.println("Driving...");
    }
}

class Coupe extends Car {

    @Override
    public void Drive() {
        System.out.println("Driving fast...");
    }
}

We don’t explicitly have to use the @Override annotation.

Example:
public class Program {
    public static void main(String[] args) {

        Car car = new Car();
        car.Drive();

        Coupe coupe = new Coupe();
        // same method name, but
        // the message printed is
        // different
        coupe.Drive();
    }
}

class Car {

    public void Drive() {
        System.out.println("Driving...");
    }
}

class Coupe extends Car {

    public void Drive() {
        System.out.println("Driving fast...");
    }
}

The example above produces the same results as the one with the annotation. If we do use the annotation, Java will enforce some rules.

  • The parent class and the child class must have the same method name, return type and parameter list.
  • Methods marked as final or static cannot be overridden.

We always recomment being as explicit and verbose as possible. The annotation will make it easy to see at a glance that the method is being overridden.

Abstract classes and methods in Java

The abstract modifier indicates that a class or a member is missing its implementation. This is similar to an interface, which we look at in the Interfaces lesson .

An abstract method doesn’t have a body, instead it looks similar to a method call. Both the abstract method and the class it’s in is marked with the abstract keyword.

Syntax:
abstract class parent_class {

    // abstract method
    return_type abstract method_name(parameters);
}

We don’t use abstract classes and methods directly, in fact Java won’t allow abstract class instantiation.

Instead, we inherit from an abstract class and in the class we override the abstract method, thereby providing its implmentation.

Syntax:
abstract class parent_class {

    // abstract method
    return_type abstract method_name(parameters);
}

class child_class extends parent_class {

    // override abstract method
    return_type method_name(parameters) {

        // body
    }
}

As before, we may choose to the @Override annotation or not.

Syntax:
abstract class parent_class {

    // abstract method
    return_type abstract method_name(parameters);
}

class child_class extends parent_class {

    @Override
    return_type method_name(parameters) {

        // body
    }
}
Example:
public class Program {
    public static void main(String[] args) {

        Coupe coupe = new Coupe();
        coupe.Drive();
    }
}

// the class contains an
// abstract method so it
// must be marked as
// abstract as well
abstract class Car {

    // an abstract method
    // doesn't have a body
    public abstract void Drive();
}

class Coupe extends Car {

    // the overriding method
    // in the child class
    // provides the body
    @Override
    public void Drive() {
        System.out.println("Driving fast...");
    }
}

In the example above, we marked the ‘Car’ class and its ‘Drive’ method as abstract. The ‘Coupe’ child class then had to override the method to provide an implementation for it.

Why use abstract classes in Java

Because abstract classes are so strict, we can provide common behaviour, while forcing other developers to follow our design.

In our example above, we force a developer to provide a ‘Drive’ method for whichever car they’re designing. If we didn’t, they might make the car fly. Ahem, mr. Musk…

Final classes and methods in Java

Final classes are the opposite of abstract classes. The final modifier prevents derivation of classes and overriding of methods.

Final classes in Java are similar to sealed classes in C#.

The final modifier provides us with the following functionality:

  • We cannot inherit from a class marked as final.
  • We cannot override a method marked as final.
  • When a variable is marked as final, it becomes a constant.

To prevent a class from being inherited, we simply mark it with the final keyword.

Syntax:
final class class_name {
    // class body
}
Example:
public class Program {
    public static void main(String[] args) {

        Car car = new Car();
        car.Drive();
    }
}

final class Car {

    public void Drive() {
        System.out.println("Driving...");
    }
}

// because the 'Car' class
// is marked as final, the
// inheritance here will
// raise a warning
class Coupe extends Car {}

In the example above, we marked the ‘Car’ class as final. When we try to inherit from it with the ‘Coupe’ class, the compiler raises a warning.

Output:
 The type Coupe cannot subclass the final class Car

The warning doesn’t stop the execution of the program, it simply tells us that its not possible.

If the final modifier is applied to a method, it prevents the method from being overridden.

Syntax:
class class_name {

    access_modifier final method_name(parameters) {
        // method body
    }
}
Example:
public class Program {
    public static void main(String[] args) {

        Car car = new Car();
        car.Drive();

        Coupe coupe = new Coupe();
        coupe.Drive();
    }
}

class Car {

    public final void Drive() {
        System.out.println("Driving...");
    }
}

class Coupe extends Car {

    @Override
    public void Drive() {
        System.out.println("Driving fast...");
    }
}

In the example above, we try to override the final method in the ‘Car’ parent class with the one in the ‘Coupe’ child class, but the compiler raises an error.

Output:
 cannot override the final method from Car

Why use final classes in Java

Final classes are slightly faster because of run-time optimizations.

However, finalizing classes and methods is one of those features that’s hardly ever used because it can mess up the inheritance hierarchy.

Summary: Points to remember

  • Polymorphism is a big scary word that simply means we override members of a parent class from within a child class, or overload methods with different or enhanced functionality.
  • An abstract method doesn’t have an implementation (a body). The inheriting class must override the method.
    • If a method is marked as abstract, the class itself must be marked as abstract as well.
    • An abstract method definition looks like a normal method call.
  • The final modifier blocks a class from being inherited.
    • If the final modifier is applied to a method, it blocks the method from being overridden.