Method Overriding in Java

Introduction

Object-Oriented Programming (OOP) encourages us to model real-world objects in code. And the thing with objects is that some share outward appearances. Also, a group of them may display similar behavior.

Java is an excellent language to cater to OOP. It allows objects to inherit the common characteristics of a group. It allows them to offer their unique attributes too. This not only makes for a rich domain, but also one that can evolve with the business needs.

When a Java class extends another, we call it a subclass. The one extended from becomes a superclass. Now, the primary reason for this is so that the subclass can use the routines from the superclass. Yet, in other cases the subclass may want to add extra functionality to what the superclass already has.

With method overriding, inheriting classes may tweak how we expect a class type to behave. And as this article will show, that is the foundation for one of OOP's most powerful and important mechanisms. It is the basis for polymorphism.

What is Method Overriding?

Generally, when a subclass extends another class, it inherits the behavior of the superclass. The subclass also gets the chance to change the capabilities of the superclass as needed.

But to be precise, we call a method as overriding if it shares these features with one of its superclass' method:

  1. The same name
  2. The same number of parameters
  3. The same type of parameters
  4. The same or covariant return type

To better understand these conditions, take a class Shape. This is a geometric figure, which has a calculable area:

abstract class Shape {
    abstract Number calculateArea();
}

Let's then extend this base class into a couple of concrete classes — a Triangle and a Square:

class Triangle extends Shape {
    private final double base;
    private final double height;

    Triangle(double base, double height) {
        this.base = base;
        this.height = height;
    }

    @Override
    Double calculateArea() {
        return (base / 2) * height;
    }

    @Override
    public String toString() {
        return String.format(
                "Triangle with a base of %s and height of %s",
                new Object[]{base, height});
    }
}

class Square extends Shape {
    private final double side;

    Square(double side) {
        this.side = side;
    }

    @Override
    Double calculateArea() {
        return side * side;
    }

    @Override
    public String toString() {
        return String.format("Square with a side length of %s units", side);
    }
}

Besides overriding the calculateArea() method, the two classes override Object's toString() as well. Also note that the two annotate the overridden methods with @Override.

Because Shape is abstract, the Triangle and the Square classes must override calculateArea(), as the abstract method offers no implementation.

Yet, we also added a toString() override. The method is available to all objects. And since the two shapes are objects, they can override toString(). Though it is not mandatory, it makes printing out a class' details human-friendly.

And this comes in handy when we want to log or print out a class' description when testing, for instance:

void printAreaDetails(Shape shape) {
    var description = shape.toString();
    var area = shape.calculateArea();

    // Print out the area details to console
    LOG.log(Level.INFO, "Area of {0} = {1}", new Object[]{description, area});
}

So, when you run a test such as:

void calculateAreaTest() {
    // Declare the side of a square
    var side = 5;

    // Declare a square shape
    Shape shape = new Square(side);

    // Print out the square's details
    printAreaDetails(shape);

    // Declare the base and height of a triangle
    var base = 10;
    var height = 6.5;

    // Reuse the shape variable
    // By assigning a triangle as the new shape
    shape = new Triangle(base, height);

    // Then print out the triangle's details
    printAreaDetails(shape);
}

You will get this output:

INFO: Area of Square with a side length of 5.0 units = 25
INFO: Area of Triangle with a base of 10.0 and height of 6.5 = 32.5

As the code shows, it is advisable to include the @Override notation when overriding. And as Oracle explains, this is important because it:

...instructs the compiler that you intend to override a method in the superclass. If, for some reason, the compiler detects that the method does not exist in one of the superclasses, then it will generate an error.

How and When to Override

In some cases, method overriding is mandatory - if you implement an interface, for example, you must override its methods. Yet, in others, it is usually up to the programmer to decide whether they will override some given methods or not.

Take a scenario where one extends a non-abstract class, for instance. The programmer is free (to some extent) to choose methods to override from the superclass.

Methods from Interfaces and Abstract Classes

Take an interface, Identifiable, which defines an object's id field:

public interface Identifiable<T extends Serializable> {
    T getId();
}

T represents the type of the class that will be used for the id. So, if we use this interface in a database application, T may have the type Integer, for example. Another notable thing is that T is Serializable.

So, we could cache, persist, or make deep copies from it.

Then, say we create a class, PrimaryKey, which implements Identifiable:

class PrimaryKey implements Identifiable<Integer> {
    private final int value;

    PrimaryKey(int value) {
        this.value = value;
    }

    @Override
    public Integer getId() {
        return value;
    }
}

PrimaryKey must override the method getId() from Identifiable. It means that PrimaryKey has the features of Identifiable. And this is important because PrimaryKey could implement several interfaces.

In such a case, it would have all the capabilities of the interfaces it implements. That is why such a relationship is called a "has-a" relationship in class hierarchies.

Let us consider a different scenario. Maybe you have an API that provides an abstract class, Person:

abstract class Person {
    abstract String getName();
    abstract int getAge();
}

So, if you wish to take advantage of some routines that only work on Person types, you'd have to extend the class. Take this Customer class, for instance:

class Customer extends Person {
    private final String name;
    private final int age;

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

    @Override
    String getName() {
        return name;
    }

    @Override
    int getAge() {
        return age;
    }
}

By extending Person using Customer, you are forced to apply overrides. Yet, it only means that you have introduced a class, which is of type Person. You have thus introduced an "is-a" relationship. And the more you look at it, the more such declarations make sense.

Because, after all, a customer is a person.

Extending a Non-final Class

Sometimes, we find classes that contain capabilities we could make good use of. Let us say you are designing a program that models a cricket game, for instance.

You have assigned the coach the task of analyzing games. Then after doing that, you come across a library, which contains a Coach class that motivates a team:

class Coach {
    void motivateTeam() {
        throw new UnsupportedOperationException();
    }
}

If Coach is not declared final, you're in luck. You can simply extend it to create a CricketCoach who can both analyzeGame() and motivateTeam():

class CricketCoach extends Coach {
    String analyzeGame() {
        throw new UnsupportedOperationException();
    }

    @Override
    void motivateTeam() {
        throw new UnsupportedOperationException();
    }
}

Extending a final Class

Finally, what would happen if we were to extend a final class?

final class CEO {
    void leadCompany() {
        throw new UnsupportedOperationException();
    }
}

And if we were to try and replicate a CEOs functionality through another class, say, SoftwareEngineer:

class SoftwareEngineer extends CEO {}

We'd be greeted with a nasty compilation error. This makes sense, as the final keyword in Java is used to point out things that shouldn't change.

You can't extend a final class.

Typically, if a class isn't meant to be extended, it's marked as final, the same as variables. Though, there is a workaround if you must go against the original intention of the class and extend it - to a degree.

Creating a wrapper class that contains an instance of the final class, which provides you with methods that can change the state of the object. Though, this works only if the class being wrapped implements an interface which means that we can supply the wrapper instead of the final class instead.

Finally, you can use a Proxy during runtime, though it's a topic that warrants an article for itself.

A popular example of a final class is the String class. It is final and therefore immutable. When you perform "changes" to a String with any of the built-in methods, a new String is created and returned, giving the illusion of change:

public String concat(String str) {
    int otherLen = str.length();
    if (otherLen == 0) {
        return this;
    }

    int len = value.length;
    char buf[] = Arrays.copyOf(value, len + otherLen);
    str.getChars(buf, len);
    return new String(buf, true);
}

Method Overriding and Polymorphism

The Merriam-Webster dictionary defines polymorphism as:

The quality or state of existing in or assuming different forms

Method overriding enables us to create such a feature in Java. As the Shape example showed, we can program it to calculate areas for varying shape types.

And more notably, we do not even care what the actual implementations of the shapes are. We simply call the calculateArea() method on any shape. It is up to the concrete shape class to determine what area it will provide, depending on its unique formula.

Polymorphism solves the many pitfalls that come with inadequate OOP designs. For example, we can cure anti-patterns such as excessive conditionals, tagged classes, and utility classes. By creating polymorphic hierarchies, we can reduce the need for these anti-patterns.

Conditionals

It is bad practice to fill code with conditionals and switch statements. The presence of these usually points to code smell. They show that the programmer is meddling with the control flow of a program.

Consider the two classes below, which describe the sounds that a Dog and a Cat make:

class Dog {
    String bark() {
        return "Bark!";
    }

    @Override
    public String toString() {
        return "Dog";
    }
}

class Cat {
    String meow() {
        return "Meow!";
    }

    @Override
    public String toString() {
        return "Cat";
    }
}

We then create a method makeSound() to make these animals produce sounds:

void makeSound(Object animal) {
    switch (animal.toString()) {
        case "Dog":
            LOG.log(Level.INFO, ((Dog) animal).bark());
            break;
        case "Cat":
            LOG.log(Level.INFO, ((Cat) animal).meow());
            break;
        default:
            throw new AssertionError(animal);
    }
}

Now, a typical test for makeSound() would be:

void makeSoundTest() {
    var dog = new Dog();
    var cat = new Cat();

    // Create a stream of the animals
    // Then call the method makeSound to extract
    // a sound out of each animal
    Stream.of(dog, cat).forEach(animal -> makeSound(animal));
}

Which then outputs:

INFO: Bark!
INFO: Meow!

While the code above works as expected, it nonetheless displays poor OOP design. We should thus refactor it to introduce an abstract Animal class. This will then assign the sound-making to its concrete classes:

abstract class Animal {
    // Assign the sound-making
    // to the concrete implementation
    // of the Animal class
    abstract void makeSound();
}

class Dog extends Animal {
    @Override
    void makeSound() {
        LOG.log(Level.INFO, "Bark!");
    }
}

class Cat extends Animal {
    @Override
    void makeSound() {
        LOG.log(Level.INFO, "Meow!");
    }
}

The test below then shows how simple it has become to use the class:

void makeSoundTest() {
    var dog = new Dog();
    var cat = new Cat();

    // Create a stream of animals
    // Then call each animal's makeSound method
    // to produce each animal's unique sound
    Stream.of(dog, cat).forEach(Animal::makeSound);
}

We no longer have a separate makeSound method as before to determine how to extract a sound from an animal. Instead, each concrete Animal class has overridden makeSound to introduce polymorphism. As a result, the code is readable and brief.

If you'd like to read more about Lambda Expressions and Method References shown in the code samples above, we've got you covered!

Utility Classes

Utility classes are common in Java projects. They usually look something like the java.lang.Math's min() method:

public static int min(int a, int b) {
    return (a <= b) ? a : b;
}

They provide a central location where the code can access often-used or needed values. The problem with these utilities is that they do not have the recommended OOP qualities. Instead of acting like independent objects, they behave like procedures. Hence, they introduce procedural programming into an OOP ecosystem.

Like in the conditionals scenario, we should refactor utility classes to introduce polymorphism. And an excellent starting point would be to find common behavior in the utility methods.

Take the min() method in the Math utility class, for instance. This routine seeks to return an int value. It also accepts two int values as input. It then compares the two to find the smaller one.

So, in essence, min() shows us that we need to create a class of type Number - for convenience, named Minimum.

In Java, the Number class is abstract. And that is a good thing. Because it will allow us to override the methods that are relevant to our case alone.

It will, for instance, give us the chance to present the minimum number in various formats. In addition to int, we could also offer the minimum as long, float, or a double. As a result, the Minimum class could look like this:

public class Minimum extends Number {

    private final int first;
    private final int second;

    public Minimum(int first, int second) {
        super();
        this.first = first;
        this.second = second;
    }

    @Override
    public int intValue() {
        return (first <= second) ? first : second;
    }

    @Override
    public long longValue() {
        return Long.valueOf(intValue());
    }

    @Override
    public float floatValue() {
        return (float) intValue();
    }

    @Override
    public double doubleValue() {
        return (double) intValue();
    }
}

In actual usage, the syntax difference between Math's min and Minimum is considerable:

// Find the smallest number using
// Java's Math utility class
int min = Math.min(5, 40);

// Find the smallest number using
// our custom Number implementation
int minimumInt = new Minimum(5, 40).intValue();

Yet an argument that one may present against the approach above is that it is more verbose. True, we may have expanded the utility method min() to a great extent. We have turned it into a fully-fledged class, in fact!

Some will find this more readable, while some will find the previous approach more readable.

Overriding vs Overloading

In a previous article, we explored what method overloading is, and how it works. Overloading (like overriding) is a technique for perpetuating polymorphism.

Only that in its case, we do not involve any inheritance. See, you will always find overloaded methods with similar names in one class. In contrast, when you override, you deal with methods found across a class type's hierarchy.

Another distinguishing difference between the two is how compilers treat them. Compilers choose between overloaded methods when compiling and resolve overridden methods at runtime. That is why overloading is also known as compile-time polymorphism. And we may also refer to overriding as runtime polymorphism.

Still, overriding is a better than overloading when it comes to realizing polymorphism. With overloading, you risk creating hard-to-read APIs. In contrast, overriding forces one to adopt class hierarchies. These are especially useful because they force programmers to design for OOP.

In summary, overloading and overriding differ in these ways:

Method Overloading Method Overriding
Does not require any inheritance. Overloaded methods occur in a single class. Works across class hierarchies. It thus occurs in several related classes.
Overloaded methods do not share method signatures. Whereas the overloaded methods must share the same name, they should differ in the number, type, or order of parameters. Overridden methods have the same signature. They have the same number and order of parameters.
We do not care what an overloaded method returns. Thus, several overloaded methods may feature vastly dissimilar return values. Overridden methods must return values that share a type.
The type of exceptions that overloaded methods throw do not concern the compiler Overridden methods should always feature the same number of exceptions as the superclass or fewer

Conclusion

Method overriding is integral to the presentation of Java's OOP muscle. It cements class hierarchies by allowing subclasses to possess and even extend the capabilities of their superclasses.

Still, most programmers encounter the feature only when implementing interfaces or extending abstract classes. Non-mandatory overriding can improve a class' readability and consequent usability.

For instance, you are encouraged to override the toString() method from the class Object. And this article displayed such practice when it overrode toString() for the Shape types - Triangle and Square.

Finally, because method overriding combines inheritance and polymorphism, it makes an excellent tool for removing common code smells. Issues, such as, excessive conditionals and utility classes could become less prevalent through wise use of overriding.

As always, you can find the entire code on GitHub.