DEV Community

Naresh Joshi
Naresh Joshi

Posted on

Why We Should Follow Method Overriding Rules

In my previous articles Why Should We Follow Method Overloading Rules, I discussed method overloading and rules we need to follow to overload a method. I have also discussed why we need to follow these rules and why some method overloading rules are necessary and others are optional.

In a similar manner in this article, we will see what rules we need to follow to override a method and why we should follow these rules.

Method Overriding and its Rules

As discussed in Everything About Method Overloading Vs Method Overriding, every child class inherits all the inheritable behaviour from its parent class but the child class can also define its own new behaviours or override some of the inherited behaviours.

Overriding means redefining a behaviour (method) again in the child class which was already defined by its parent class but to do so overriding method in the child class must follow certain rules and guidelines.

With respect to the method it overrides, the overriding method must follow the following rules.

Why We Should Follow Method Overriding Rules

To understand these reasons properly let's consider below example where we have a class Mammal which defines readAndGet method which is reading some file and returning an instance of class Mammal.

Class Human extends class Mammal and overrides readAndGet method to return the instance of Human instead of an instance of Mammal.

class Mammal {
    public Mammal readAndGet() throws IOException {//read file and return Mammal`s object}
}

class Human extends Mammal {
    @Override
    public Human readAndGet() throws FileNotFoundException {//read file and return Human object}
}
Enter fullscreen mode Exit fullscreen mode

And we know in case of method overriding we can make polymorphic calls. Which means if we assign a child instance to a parent reference and call an overridden method on that reference eventually the method from child class will get called. Let's do that

Mammal mammal = new Human();
try {
    Mammal obj = mammal.readAndGet();
} catch (IOException ex) {..}
Enter fullscreen mode Exit fullscreen mode

As discussed in  How Does JVM Handle Method Overloading and Overriding Internally till compilation phase compiler thinks the method is getting called from the parent class. While bytecode generation phase compiler generates a constant pool where it maps every method string literal and class reference to a memory reference

During runtime, JVM creates a vtable or virtual table to identify which method is getting called exactly. JVM creates a vtable for every class and it is common for all the objects of that class. Mammal row in a vtable contains method name and memory reference of that method.

First JVM creates a vtable for the parent class and then copy that parent's vtable to child class's vtable and update just the memory reference for the overloaded method while keeping the same method name.

You can read it more clearly on  How Does JVM Handle Method Overloading and Overriding Internally if it seems hard. So as of now, we are clear that

  • For compiler mammal.readAndGet() means method is getting called from instance of class Mammal
  • For JVM mammal.readAndGet() is getting called from a memory address which vtable is holding for Mammal.readAndGet() which is pointing to a method call from class Human.

Why overriding method must have the same name and same argument list

Well conceptually mammal is pointing to an object of class Human and we are calling readAndGet method on mammal, so to get this call resolved at runtime Human should also have a method readAndGet. And if Human has inherited that method from Mammal then there is no problem but if Human is overriding readAndGet, it should provide the same method signature as provided by Mammal because the method has been already got called according to that method signature.

But you may be asking how it is handled physically from vtables so I must tell you that, JVM creates a vtable for every class and when it encounters an overriding method it keeps the same method name (Mammal.readAndGet()) while just update the memory address for that method. So both overridden and overriding method must have the same method and argument list.

Why overriding method must have the same or covariant return type

So we know, for compiler the method is getting called from class Mammal and for JVM call is from the instance of class Human but in both cases, readAndGet method call must return an object which can be assigned to obj. And since obj is of the type Mammal it can either hold an instance of Mammal class or an instance of a child class of Mammal (child of Mammal are covariant to Mammal).

Now suppose if readAndGet method in Human class is returning something else so during compile time mammal.readAndGet() will not create any problem but at runtime, this will cause a ClassCastException because at runtime mammal.readAndGet() will get resolved to new Human().readAndGet() and this call will not return an object of type Mammal.

And this why having a different return type is not allowed by the compiler in the first place.

Why overriding method must not have a more restrictive access modifier

The same logic is applicable here as well, call to readAndGet method will be resolved at runtime and as we can see readAndGet is public in class Mammal, now suppose

  • If we define readAndGet as default or protected in Human but Human is defined in another package
  • If we define readAndGet as private in Human

In both cases, the code will compile successfully because for compiler readAndGet is getting called from class Mammal but in both cases, JVM will not be able to access readAndGet from Human because it will be restricted.

So to avoid this uncertainty, assigning restrictive access to the overriding method in the child class is not allowed at all.

Why overriding method may have less restrictive access modifier

If readAndGet method is accessible from Mammal and we are able to execute mammal.readAndGet() which means this method is accessible. And we make readAndGet less restrictive Human which means it will be more open to getting called.

So making the overriding method less restrictive cannot create any problem in the future and that's it is allowed.

Why overriding method must not throw new or broader checked exceptions

Because IOException is a checked exception compiler will force us to catch it whenever we call readAndGet on mammal

Now suppose readAndGet in Human is throwing any other checked exception e.g. Exception and we know readAndGet will get called from the instance of Human because mammal is holding new Human().

Because for compiler the method is getting called from Mammal, so the compiler will force us to handle only IOException but at runtime we know method will be throwing Exception which is not getting handled and our code will break if the method throws an exception.

That's why it is prevented at the compiler level itself and we are not allowed to throw any new or broader checked exception because it will not be handled by JVM at the end.

Why overriding method may throw narrower checked exceptions or any unchecked exception

But if readAndGet in Human throws any sub-exception of IOException e.g., FileNotFoundException, it will be handled because catch (IOException ex) can handle all child of IOException.

And we know unchecked exception (subclasses of RuntimeException) are called unchecked because we don't need to handle them necessarily.

And that's why overriding methods are allowed to throw narrower checked and other unchecked exceptions.

To force our code to adhere method overriding rules we should always use @Override annotation on our overriding methods, @Override annotation force compiler to check if the method is a valid override or not.

You can find the complete code on this Github Repository and please feel free to provide your valuable feedback.

Top comments (0)