Complete Guide to Method Overriding in Java

Sridhar KSridhar K
11 min read

Method Overriding

Method overriding, also known as run-time polymorphism, is used to override methods of a parent class. This allows the child class to provide a specific implementation for a method that is already defined in its parent class. By doing so, it gives the child class the special ability to modify the behavior of the parent class's methods.

Method overriding can be done using the @Overrideannotation. This annotation helps the Java compiler understand that the specific method in the child class is an overridden method. It allows the child class to modify the method's body, providing a different implementation than that of the parent class's method.

Syntax for Method Overriding

class ParentClass {
    // Parent class method
    void display() {
        // Method implementation
        System.out.println("Inside Parent Class Display");
    }
}

class ChildClass extends ParentClass {
    // Overriding the parent class method
    @Override
    void display() {
        // Modified method implementation
        System.out.println("Inside Child Class Display");
    }
}

Here, ParentClass acts as a parent class that has a method named display(), and ChildClass is a child class of the ParentClass (the extends keyword is used to express their parent-child relationship). This ChildClass has an overridden method display(), where you can modify the method's body according to the child class's requirements.

Note: To perform method overriding, both the parent class method and the child class method should have the same name. If both the methods have dissimilar names then you can see an error message while using @override annotation.

This example will clearly show you the error message that appears if you change the method's name in the child class.

class ParentClass {
    // Parent class method
    void display() {
        System.out.println("This is the display method of ParentClass.");
    }
}

class ChildClass extends ParentClass {
    // Incorrect method name, should be 'display'
    @Override
    void show() {
        System.out.println("This is the show method of ChildClass.");
    }
}

When you try to compile this code, you will get an error message similar to the following:

ChildClass.java:7: error: method does not override or implement a method from a supertype
    @Override
    ^
1 error

Note: Private methods can never be overriden in java

class ParentClass {
    // Private method in the parent class
    private void display() {
        System.out.println("This is the display method of ParentClass.");
    }
}

class ChildClass extends ParentClass {
    // Attempt to override the private method
    @Override
    private void display() {
        System.out.println("This is the display method of ChildClass.");
    }
}

When you try to compile this code, you will get an error message similar to the following:

ChildClass.java:7: error: method does not override or implement a method from a supertype
    @Override
    ^
1 error

This error occurs because the display method in ParentClass is private and cannot be overridden in ChildClass. In real life, a master chef would never share his recipe with outsiders.

Dynamic Polymorphism

Dynamic Polymorphism, also known as runtime polymorphism, is achieved through method overriding. This occurs when a subclass provides a specific implementation of a method already provided by its superclass.

Consider a score-generating application like ESPN, which provides scorecards for different games. The basic functionality of updating a scorecard is common, but the specifics may vary based on the game. For instance, a Football class should update football scores, and a Cricket class should update cricket scores.

// Parent class
class ScoreGenerator {
    void updateScore() {
        System.out.println("Updating score...");
    }
}

// Child class for Football
class FootballScore extends ScoreGenerator {
    @Override
    void updateScore() {
        System.out.println("Updating football score...");
        // Specific implementation for football scores
    }
}

// Child class for Cricket
class CricketScore extends ScoreGenerator {
    @Override
    void updateScore() {
        System.out.println("Updating cricket score...");
        // Specific implementation for cricket scores
    }
}

// Child class for Hockey
class HockeyScore extends ScoreGenerator {
    @Override
    void updateScore() {
        System.out.println("Updating hockey score...");
        // Specific implementation for hockey scores
    }
}

In this example:

  • ScoreGenerator is the parent class with a method updateScore() that prints a generic message.

  • FootballScore, CricketScore, and HockeyScore are child classes that extend ScoreGenerator and override the updateScore() method to provide specific implementations for football, cricket, and hockey scores respectively.

Each child class (FootballScore, CricketScore, HockeyScore) inherits the updateScore() method from the ScoreGenerator parent class but provides its own unique implementation suitable for updating scores specific to football, cricket, and hockey.

Example for Method Overriding

Here's an example that illustrates the concept of method overriding:

// Parent class
class ScoreGenerator {
    void updateScore() {
        System.out.println("Updating score...");
    }
}

// Child class for Football
class FootballScore extends ScoreGenerator {
    @Override
    void updateScore() {
        System.out.println("Updating football score...");
        // Specific implementation for football scores
    }
}

// Child class for Cricket
class CricketScore extends ScoreGenerator {
    @Override
    void updateScore() {
        System.out.println("Updating cricket score...");
        // Specific implementation for cricket scores
    }
}

// Child class for Hockey
class HockeyScore extends ScoreGenerator {
    @Override
    void updateScore() {
        System.out.println("Updating hockey score...");
        // Specific implementation for hockey scores
    }
}

// Main class to demonstrate usage
public class Main {
    public static void main(String[] args) {
        ScoreGenerator footballScore = new FootballScore();
        footballScore.updateScore(); // Calls FootballScore's updateScore method

        ScoreGenerator cricketScore = new CricketScore();
        cricketScore.updateScore(); // Calls CricketScore's updateScore method

        ScoreGenerator hockeyScore = new HockeyScore();
        hockeyScore.updateScore(); // Calls HockeyScore's updateScore method
    }
}

In this example:

  • ScoreGenerator: This is the parent class that defines a method updateScore(). The method updateScore() is implemented to print a generic message "Updating score...".

  • FootballScore, CricketScore, HockeyScore: These are child classes of ScoreGenerator. Each class extends ScoreGenerator and overrides the updateScore() method with its own specific implementation:

    • FootballScore: Overrides updateScore() to print "Updating football score...".

    • CricketScore: Overrides updateScore() to print "Updating cricket score...".

    • HockeyScore: Overrides updateScore() to print "Updating hockey score...".

    • Each overridden method could contain additional logic specific to updating scores for football, cricket, or hockey, respectively.

  • Main Class:

    • Dynamic Polymorphism: In the main method, we create instances of each child class (FootballScore, CricketScore, HockeyScore) and assign them to ScoreGenerator references (footballScore, cricketScore, hockeyScore).

    • Each time updateScore() is called on these references, Java uses dynamic method dispatch to call the overridden updateScore() method specific to the actual object type (FootballScore, CricketScore, HockeyScore).

    • Therefore, footballScore.updateScore() calls FootballScore's updateScore() method, cricketScore.updateScore() calls CricketScore's updateScore() method, and hockeyScore.updateScore() calls HockeyScore's updateScore() method.

Rules For Method Overriding

Rule 1: The access modifier type specified in the parent class must match that of the overridden method in the child class, otherwise you will encounter an error.

// Parent class
class Parent {
    protected void display() {
        System.out.println("Inside parent class display");
    }
}

// Child class extending Parent
class Child extends Parent {
    protected void display() {
        System.out.println("Inside child class display");
    }
}

// Main class to demonstrate method invocation
public class App {
    public static void main(String[] args) {
        Child ch = new Child();
        ch.display(); // Calls Child's display method
    }
}

In order to perform method overriding in Java, both the parent class method and the child class method must have the same access modifier.

In the given example:

  • The Parent class defines a protected method display() that prints "Inside parent class display".

  • The Child class extends Parent and overrides the display() method with its own protected version that prints "Inside child class display".

In the App class:

  • An instance of Child (ch) is created.

  • When ch.display() is called, it invokes the display() method from Child, demonstrating method overriding where the behavior of the method is based on the type of the actual object (Child) at runtime.

Rule 2: In method overriding, the visibility or access modifiers of the parent class method and the overridden method in the child class can be different, provided that the visibility of the overridden method in the child class is the same or more accessible than that of the parent class method.

If the visibility is decreased (i.e., made more restrictive) in the child class, it will result in a compile-time error.

// Parent class
class Parent {
    protected void display() {
        System.out.println("Inside parent class display");
    }
}

// Child class extending Parent
class Child extends Parent {
    // Attempting to reduce visibility (compile-time error)
    private void display() { // error
        System.out.println("Inside child class display");
    }
}

// Main class to demonstrate method invocation
public class App {
    public static void main(String[] args) {
        Child ch = new Child();
        ch.display(); // Compilation error: Attempting to assign weaker access privileges; was protected
    }
}

The Child class attempts to override display() with private access, which is more restrictive than protected. This violates the rule that the visibility of the overriding method must be the same or broader than that of the overridden method. Therefore, the Java compiler generates a compilation error indicating that it is "Attempting to assign weaker access privileges".

To correct this, the access modifier of display() in the Child class should either match (protected) or widen (public) the access modifier of display() in the Parent class to properly override the method without errors.

Note:

  • If the parent class method is public, the child class overridden method can also be public.

  • If the parent class method is protected, the child class overridden method can be protected or public.

  • If the parent class method is default (package-private), the child class overridden method can be default, protected, or public; it can never be private.

Rule 3: The return type of both the parent class method and the child class method should be the same; if they are different, method overriding is not allowed.

Example

// Parent class
class Parent {
    int display() {
        System.out.println("Inside parent class display");
        return 10;
    }
}

// First child class extending Parent
class Child1 extends Parent {
    // Error: Attempting to change return type to float
    @Override
    public float display() {
        System.out.println("Inside Child1 class display");
        return 1.5f; // Compile-time error: Return type mismatch
    }
}

// Second child class extending Parent
class Child2 extends Parent {
    // Correctly overriding with the same return type
    @Override
    int display() {
        System.out.println("Inside Child2 class display");
        return 4;
    }
}

// Main class to demonstrate method invocation
public class Main {
    public static void main(String[] args) {
        Parent obj1 = new Child1();
        obj1.display(); // Error at compile time due to return type mismatch

        Parent obj2 = new Child2();
        obj2.display(); // Calls Child2's display method
    }
}

The program illustrates the rule that in method overriding, the return type of the overridden method in the child class must be the same as (or covariant with) the return type of the method in the parent class. Attempting to change the return type in the child class (Child1) results in a compilation error, while correctly matching the return type (Child2) allows for successful method overriding.

Rule 4: n method overriding, the parent class method and the child overridden method can have different return types, but they must be co-variant return types, meaning there should be a parent-child relationship between them.

// Parent class representing electronic devices
class ElectronicDevice {
}

// Subclass extending ElectronicDevice, representing a specific type of electronic device
class Laptop extends ElectronicDevice {
}

// Parent class with a method to display electronic devices
class Parent {
    // Method in Parent class to display an ElectronicDevice
    ElectronicDevice display() {
        System.out.println("Parent class display");
        return new ElectronicDevice();
    }
}

// Child class extending Parent, overriding the display method
class Child1 extends Parent {
    // Override of display method in Child1
    @Override
    ElectronicDevice display() {
        System.out.println("Child1 class display");
        return new ElectronicDevice();
    }
}

// Another Child class extending Parent, with a different override of the display method
class Child2 extends Parent {
    // Override of display method in Child2, returning a Laptop
    @Override
    ElectronicDevice display() {
        System.out.println("Child2 class display");
        return new Laptop();
    }
}

// Main class to demonstrate method invocation and polymorphism
public class Main {
    public static void main(String[] args) {
        // Creating an instance of Child1 and invoking display method
        Parent obj1 = new Child1();
        obj1.display(); // Calls Child1's display method

        // Creating an instance of Child2 and invoking display method
        Parent obj2 = new Child2();
        obj2.display(); // Calls Child2's display method
    }
}

Covariant return types in Java allow a method in a subclass to return a more specific type than the method in its superclass. In our example:

  • The Parent class defines display() returning ElectronicDevice.

  • Child1 and Child2 override display() returning ElectronicDevice and Laptop respectively, maintaining a hierarchical relationship where Laptop is a subtype of ElectronicDevice. This is an example of covariant return types in Java.

Advantages of Method Overriding

  • Polymorphism: It allows a subclass to provide a specific implementation of a method that is already defined in its superclass. This means that a method can behave differently based on the object that invokes it, enabling polymorphic behavior.

  • Code Reusability: By overriding methods, subclasses can reuse code defined in their superclasses. There is no need to re write those lines. This promotes code reuse and reduces redundancy in software development. This helps us to overcome code redundancy.

  • Enhanced Readability: Method overriding makes code easier to understand by letting developers use clear method names in subclasses that show exactly what each method does, making the codebase clearer overall.

  • Flexibility: Method overriding allows subclasses to adapt inherited methods to meet their specific needs without directly altering the superclass code, thus offering greater flexibility in class design and behavior customization.

Conclusion

In this blog, we've explored method overriding in Java, where a subclass can customize the behavior of a method inherited from its superclass.

Throughout our discussion, we've emphasized key rules for method overriding: ensuring method signatures match, using appropriate access modifiers, and respecting return type covariance. These rules help subclasses extend and adjust inherited functionality without disrupting the class hierarchy.

Method overriding promotes code reusability by reducing redundancy, improves code readability with clear method naming, and offers flexibility in designing class behaviors. Following these principles enables developers to create robust Java applications that capitalize on object-oriented programming and polymorphism.

In summary, method overriding in Java is essential for scalable and adaptable software development. It empowers developers to efficiently manage code and expand class functionalities while maintaining structure and ease of use.

0
Subscribe to my newsletter

Read articles from Sridhar K directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Sridhar K
Sridhar K

I am a fresher Software developer with a strong foundation in Java, HTML, CSS, and JavaScript. I have experience with various libraries and frameworks, including Spring Boot, Express.js, Node.js, Hibernate, MySQL, and PostgreSQL. I am eager to leverage my technical skills and knowledge to contribute effectively to a dynamic development team. I am committed to continuous learning and am excited to begin my career in a challenging developer role.