Java #14 - Inheritance (Part - 2)

• Method overriding • Dynamic method dispatch • Why overridden methods? • Abstract Class • Using final with inheritance

Method Overriding

In a class hierarchy, when a method in a subclass has the same name and type signature as a method in its superclass, then the method in the subclass is said to override the method in the superclass.

When an overridden method is called from within its subclass, it will always refer to the version of that method defined by the subclass.

Method overriding occurs only when the two methods' names and signatures are the same. If they are not, then two methods are overloaded.

public class A {
    int a;

    A(int a){
        System.out.println("Inside class A constructor");
        this.a =a;
    }

    void displayA(){
        System.out.println("Value of a is: "+a);
    }

    void show(){
        System.out.println("Value of a: "+a);
    }
}

public class B extends A{
    int b;

    B(int a, int b) {
        super(a);
        System.out.println("Inside class B constructor");
        this.b = b;
    }

    void displayB(){
        System.out.println("Value of b is: " + b);
    }

    void show(){
        System.out.println("Value of b: "+b);
    }
}

public class MethodOverridingDemo {
    public static void main(String[] args) {
        B b = new B(10,20);
        b.show(); // this will call show() method defined in class B
    }
}

Dynamic method dispatch

Dynamic method dispatch also known as run-time polymorphism is a mechanism by which a call to an overridden method is resolved at run time.

As we know that a superclass reference variable can refer to a subclass object, this is used to resolve calls to overridden methods at runtime. When an overridden method is called through a superclass reference, JVM determines which version of that method to execute based upon the type of the object being referred to at the time that call occurs.

public class A {
    int a;

    A(int a){
        System.out.println("Inside class A constructor");
        this.a =a;
    }
    A(){}

    void displayA(){
        System.out.println("Value of a is: "+a);
    }

    void show(){
        System.out.println("Inside class A");
    }
}

public class B extends A{
    int b;

    B(int a, int b) {
        super(a);
        System.out.println("Inside class B constructor");
        this.b = b;
    }

    B(){}

    void displayB(){
        System.out.println("Value of b is: " + b);
    }

    void show(){
        System.out.println("Inside class B");
    }
}

public class C extends B{
    int c;

    C(int a, int b,int c) {
        super(a,b);
        System.out.println("Inside class C constructor");
        this.c = c;
    }

    C() {}

    void displayC(){
        System.out.println("Value of c is: " + c);
    }

    void displayAll(){
        displayA();
        displayB();
        displayC();
    }
    void show(){
        System.out.println("Inside class C");
    }
}

public class DynamicMethodDispatchDemo {
    public static void main(String[] args) {
        A a = new A();
        B b = new B();
        C c = new C();

        A r;

        r = a;
        r.show();

        r = b;
        r.show();

        r = c;
        r.show();
    }
}

Output

Inside class A

Inside class B

Inside class C


Why overridden methods?

Method overriding is also known as dynamic or run-time polymorphism. This allows a general class to specify methods that will be common to all of its derivatives while allowing subclasses to define the specific implementations of some or all of those methods.

Class hierarchy as part of the inheritance is based on the concept of lesser to greater specialization from superclass to subclass. The superclass provides all elements that a subclass can use directly and it also defines those methods that the derived class must implement on its own. This allows the subclass the flexibility to define its own methods, yet still enforces a consistent interface.

Using inheritance with overridden methods, a superclass can define the general form of the methods that will be used by all of its subclasses.

Run-time polymorphism is one of the most powerful mechanisms that object-oriented design brings for code reuse and robustness. When a new subclass is created that overrides a method from a superclass, existing code libraries that call methods on instances of the new subclass can do so without needing to be recompiled. This is because the overridden method maintains the same name, parameters, and return type as the original method in the superclass, so the calling code does not need to be aware of the specific implementation details of the subclass.

This ability to exist code libraries to call methods on instances of new classes without recompiling while maintaining a clean abstract interface is a profoundly powerful tool.

This feature of Java makes it easier to maintain a clean and abstract interface, which is beneficial for several reasons.

  • It allows for easier maintenance and debugging of code, as changes to a subclass do not affect the calling code in the superclass.

  • It promotes code reuse, as subclasses can inherit and modify the behaviour of methods from their superclass, without the need to rewrite code from scratch.

  • It enables the creation of complex object-oriented systems, where subclasses can be added and modified over time without breaking existing code that depends on them.

public class Figure {
    double dim1;
    double dim2;

    Figure(double a, double b){
        this.dim1 = a;
        this.dim2 = b;
    }
    double area(){
        System.out.println("Area for Figure is undefined");
        return -1;
    }
}

public class Rectangle extends Figure{
    Rectangle(double a, double b){
        super(a,b);
    }
    double area(){
        System.out.println("Inside Area for Rectangle");
        return dim1 * dim2;
    }
}

public class Triangle extends Figure{
    Triangle(double a, double b){
        super(a,b);
    }

    double area(){
        System.out.println("Inside area for triangle");
        return dim1 * dim2 / 2;
    }
}

public class FindAreas {
    public static void main(String[] args) {
        Figure f = new Figure(10,10);
        Rectangle r = new Rectangle(9,5);
        Triangle t = new Triangle(10,8);

        Figure figure;

        figure = r;
        System.out.println("Area is " + figure.area());

        figure = t;
        System.out.println("Area is " + figure.area());

        figure = f;
        System.out.println("Area is " + figure. Area());
    }
}

Output

Inside Area for Rectangle

Area is 45.0

Inside area for triangle

Area is 40.0

Area for Figure is undefined

Area is -1.0


Abstract Class

We can define a superclass that declares the structure of a given abstraction without providing a complete implementation of every method i.e. only declare methods without definition. In this way superclass only defines a generalized form that will be shared by all of its subclasses, leaving it to each subclass to fill in its details this is also needed for situations when a superclass is unable to create a meaningful implementation for a method.

In the previous section example, the Figure class used has the area method as a simple placeholder as we can calculate the area of such an object so it doesn't make sense to provide any implementation of the area method in the Figure class. We can achieve this using the abstract method.

abstract type name(parameter-list); // note that there is no curly braces here it means this method is only declared not defined

  • A class declared that contains one or more abstract methods must also be declared abstract.

  • There can be no object of an abstract class because abstract does not provide implementations of all methods.

  • an abstract keyword cannot be used with constructor and static methods.

  • Any subclass of an abstract class must either implement all of the abstract methods declared in the superclass or be declared abstract itself.

Below is an improved version of the Figure class shown in the previous example, Rectangle and Triangle classes remain the same.

public abstract class Figure {
    double dim1;
    double dim2;

    Figure(double a, double b){
        this.dim1 = a;
        this.dim2 = b;
    }
    abstract double area();
}

public class FindAreas {
    public static void main(String[] args) {
        // Figure f = new Figure(10,10); //this will give compile-time error as object of abstract class cannot be created
        Rectangle r = new Rectangle(9,5);
        Triangle t = new Triangle(10,8);

        Figure figure;

        figure = r;
        System.out.println("Area is " + figure.area());

        figure = t;
        System.out.println("Area is " + figure. Area());
    }
}

Using final with inheritance

  1. Using final to prevent overriding - To prevent the method from being overridden in the subclass, use final as a modifier at the start of its declaration.
class PreventOverridingWithFinalA{
    final void displayA(){

    }
}

class PreventOverridingWithFinalB extends PreventOverridingWithFinalA{
    void displayA(){ // Compile-time error as  methods cannot be overridden because this method is declared as final in super class

    }
}
  1. Using final to prevent inheritance - To prevent a class from being inherited in the subclass, use final as a modifier at the start of its declaration.
final class PreventInheritanceWithFinalA{
}

class PreventInheritanceWithFinalB extends PreventInheritanceWithFinalA { //// Compile-time error as  class cannot be inherited because this class is declared as final

}

Did you find this article valuable?

Support subodh's blog by becoming a sponsor. Any amount is appreciated!