Understanding Method Overriding in Java

In this article, we will explore a fundamental concept of object-oriented programming in Java: method overriding. To fully understand this, we first need to grasp the concept of inheritance, as method overriding is inherently linked to it.

Inheritance in Java

Inheritance is one of the pillars of object-oriented programming. In Java, inheritance allows a class to inherit the properties (attributes) and behaviors (methods) of another class. The class that inherits is called the child class (or subclass), and the class from which it inherits is called the parent class (or superclass).

Example of Inheritance

class Animal {
    void makeSound() {
        System.out.println("The animal makes a sound");
    }
}

class Dog extends Animal {
    void makeSound() {
        System.out.println("The dog barks");
    }
}

In this example, Dog inherits from Animal. The Dog class can use the methods of the Animal class, but it can also provide its own implementation for these methods.

Method Overriding

Method overriding occurs when a child class provides a specific implementation for a method that is already defined in its parent class. Overriding allows the child class to customize or extend the behavior of the inherited method.

Rules of Method Overriding

  1. Same Signature: The overridden method must have the same name, number and types of parameters, and the same order of parameters.

  2. Return Type: The return type must be the same or a covariant subtype of the return type in the parent class.

  3. Access Modifiers: The overridden method cannot have a more restrictive access modifier than the method in the parent class.

  4. Exceptions: The overridden method cannot throw broader or more general exceptions than those declared in the parent class method.

Examples of Method Overriding

Example with Covariant Return Type

class Parent {
    public Number method() {
        return 42;
    }
}

class Child extends Parent {
    @Override
    public Integer method() {
        return 42;
    }
}

In the example above, the return type Integer in the Child class is a covariant subtype of Number, allowing the method to be overridden.

Invalid Overriding Example

class Parent {
    public Number method() {
        return 42;
    }
}

class Child extends Parent {
    @Override
    public String method() {
        return "42";
    }
}

Here, attempting to override method with a return type of String results in a compilation error because String is not a covariant subtype of Number.

Overriding and Exceptions

In method overriding, the exceptions thrown by the overridden method must be compatible with the exceptions thrown by the method in the parent class. The overridden method can throw fewer exceptions or exceptions that are subtypes of the declared exceptions.

Example with Exceptions

class Parent {
    public void method() throws IOException {
        // implementation
    }
}

class Child extends Parent {
    @Override
    public void method() throws FileNotFoundException {
        // implementation
    }
}

In the example above, FileNotFoundException is a subtype of IOException, allowing the method to be overridden.

Invalid Example with Exceptions

class Parent {
    public void method() throws IOException {
        // implementation
    }
}

class Child extends Parent {
    @Override
    public void method() throws Exception {
        // implementation
    }
}

In this case, attempting to override method with a broader exception Exception results in a compilation error because Exception is not a subtype of IOException.

Complex Example of Inheritance and Method Overriding

To better illustrate method overriding, let's consider a more complex example with inheritance and overriding three methods.

class Vehicle {
    public void start() {
        System.out.println("Vehicle started");
    }

    public void accelerate() {
        System.out.println("Vehicle accelerating");
    }

    public Number getSpeed() {
        return 60;
    }
}

class Car extends Vehicle {
    @Override
    public void start() {
        System.out.println("Car started");
    }

    @Override
    public void accelerate() {
        System.out.println("Car accelerating");
    }

    @Override
    public Integer getSpeed() {
        return 120;
    }
}

public class TestVehicle {
    public static void main(String[] args) {
        Vehicle myVehicle = new Vehicle();
        myVehicle.start(); // Output: Vehicle started
        myVehicle.accelerate(); // Output: Vehicle accelerating
        System.out.println("Speed: " + myVehicle.getSpeed()); // Output: Speed: 60

        Vehicle myCar = new Car();
        myCar.start(); // Output: Car started
        myCar.accelerate(); // Output: Car accelerating
        System.out.println("Speed: " + myCar.getSpeed()); // Output: Speed: 120
    }
}

In this example, the Car class extends the Vehicle class and overrides three methods: start, accelerate, and getSpeed. In the main method, we can see how method overriding allows the specific behavior of Car to be executed even when the object is treated as a Vehicle.

Conclusion

Method overriding is a powerful tool in object-oriented programming, allowing child classes to customize and extend the behavior of parent classes. Understanding the rules and limitations of method overriding is crucial for writing robust and efficient Java code. By following the guidelines on return types, access modifiers, and exceptions, you can make the most of this feature to create well-structured and flexible applications.

I hope this article has helped clarify how method overriding works in Java. If you have any questions or comments, feel free to share them below.

0
Subscribe to my newsletter

Read articles from André Felipe Costa Bento directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

André Felipe Costa Bento
André Felipe Costa Bento

Fullstack Software Engineer.