Showing posts with label Java Inheritance. Show all posts
Showing posts with label Java Inheritance. Show all posts

Monday, December 8, 2025

Java Method Overriding: 3 Counter-Intuitive Rules You Need to Know

 

Introduction

Method overriding in Java seems straightforward at first glance. You have a method in a parent class, and you create a more specific version in a child class. Simple, right? But lurking beneath this simple definition are hidden complexities and "gotcha" rules that can easily trip up even experienced developers.

This post will distill the most surprising and impactful rules of method overriding. We won't just cover what the rules are; we'll explore the crucial why behind them, giving you a deeper, more practical understanding of how Java's object-oriented system is designed for stability and predictability.

--------------------------------------------------------------------------------

The Takeaways

1. The Golden Rule: Don't Break Your Promises to the Outside World

When overriding a method, the access modifier in the child class cannot be more restrictive than in the parent class. You can only keep the scope the same or increase it (e.g., from default to public), but you can never reduce it (e.g., from public to default).

The core rationale for this rule is to maintain a stable contract with other parts of your code that use the method. Imagine hundreds of pieces of code across your application rely on a public parent method. If a child class could override that method and make it default, all that external code would suddenly break because it would no longer have access. The rule prevents this chaos.

without overriding this method accessed by several outside people after overriding this method also should be accessed by same set of people... the people who are already accessing they should not be affected.

2. The Exception Rule: You Can't Throw New Problems at Your Callers

If a child's overriding method throws a checked exception, the parent's method must declare that it throws the same checked exception or a parent of that exception. A child method cannot introduce a completely new, undeclared checked exception.

This rule connects directly back to the principle of not breaking contracts. Code that calls the parent method is only prepared to try...catch the specific checked exceptions declared in the parent's method signature. The caller has a try-catch block designed for the parent's declared exceptions; forcing it to handle a new, unexpected checked exception from a child would break its compile-time safety contract.

It's important to note that this strict rule does not apply to unchecked exceptions (like RuntimeException and its children), as "there are no restrictions for unchecked exceptions".

if a child class method throws any checked exception compulsory parent class method should throw the same checked exception or its parent.

3. The Biggest Twist: Static Methods Can't Be Overridden

This is perhaps the most surprising rule: static methods cannot be overridden. Attempting to override a static method with a non-static one, or vice-versa, will result in a compile-time error.

So what happens if both the parent and child class methods are static? The code will compile without error, but this is not method overriding. The correct term for this behavior is Method Hiding.

The critical difference lies in how the method call is resolved:

  • Overriding (for non-static methods): Method resolution is handled by the JVM at runtime, based on the actual object's type. This is runtime polymorphism.
  • Method Hiding (for static methods): Method resolution is handled by the compiler at compile-time, based on the reference variable's type. This is compile-time polymorphism.

Consider the code parent p1 = new child(); p1.M1();. If M1 is a non-static method, overriding ensures the child's version is called. However, if M1 is a static method (method hiding), the parent's version is called because the compiler only sees the parent reference type.

To understand why the resolution behaves this way, the source offers a powerful analogy. Think of overriding as erasing something from a whiteboard and writing new data—the old data is truly gone. In contrast, think of method hiding as pinning a new chart over an old one. The new chart hides the old one, but both still exist. If you use a reference to the "new chart" (the child type), you see it. If you use a reference to the "old chart" (the parent type), you see that one instead.

it seems overriding concept applicable for static methods but it is not overriding and it is Method hiding.

--------------------------------------------------------------------------------

Conclusion

These rules, while sometimes complex, are not arbitrary. They are carefully designed to create predictable, stable, and less error-prone object-oriented systems. By enforcing clear contracts regarding access, exceptions, and method resolution, Java ensures that polymorphism works as intended without causing unexpected side effects in unrelated parts of an application.

Knowing that static methods are resolved by reference type at compile-time, how might this change your approach to designing and using utility classes in your projects?

Saturday, December 6, 2025

4 Surprising Rules About Java Method Overriding You Might Not Know

 

When we first learn about inheritance in Java, method overriding seems straightforward. A parent class defines a method, but the child class isn't satisfied with the implementation and decides to provide its own. Much like a child might disagree with a parent's choice of who they should marry(), the child class can override the parent's logic with its own. But while the concept is simple, the rules governing it are full of surprising subtleties that can trip up even experienced developers.

Let's dive into four rules of method overriding that separate the experts from the novices—and see how they can make you a more effective Java programmer.

--------------------------------------------------------------------------------

1. It’s All Decided at Runtime (And That’s Why It’s Called Polymorphism)

The first crucial rule is that the decision of which overridden method to execute is handled by the JVM at runtime, not by the compiler. This distinction is the very foundation of polymorphism in Java.

Consider a common scenario where a parent reference holds a child object:

Parent p1 = new Child();
p1.marry(); // At runtime, the JVM executes the Child's version.

When you call a method like p1.marry(), the compiler and the JVM have distinct jobs:

  • Compiler's Role: The compiler only looks at the reference type, which is Parent. It checks if a marry() method exists in the Parent class. If it does, the code compiles successfully.
  • JVM's Role: At runtime, the JVM looks at the actual object that p1 is pointing to, which is a Child object. It then checks if the Child class has an overridden version of the marry() method. If it does, the child's method is executed.

This separation of concerns is a common source of confusion, but understanding it is key to mastering polymorphism and debugging unexpected runtime behavior. This runtime resolution is why overriding is a cornerstone of dynamic behavior in Java.

In overriding method resolution is always based on runtime object... that's why overriding is also known as runtime polymorphism or dynamic polymorphism or late binding.

While the JVM handles which method to run, the rules for what that method can return are surprisingly flexible.

--------------------------------------------------------------------------------

2. Return Types Don't Always Have to Be Identical (Welcome to Covariant Returns)

A common misconception is that the return type of an overriding method must be identical to the parent method's return type. While this was the strict rule until Java 1.4, the introduction of Java 1.5 brought more flexibility.

The modern rule introduces the concept of covariant return types. This means an overriding method in a child class can have a return type that is a subtype of the parent method's return type.

For example, if a parent method returns Object, a child can override it to return a String, because String is a subtype of Object. Crucially, this relationship is one-way. A method returning String cannot be overridden to return Object, as that would be broadening the return type, which is not allowed.

There's a critical limitation to remember: this flexibility only applies to object types, not primitives. If the parent method's return type is double, the overriding child method's return type must also be double.

...child class method return type need not be same as parent method return type it's child type also allowed.

Just as return types have rules, so do the access modifiers you can apply to an overridden method.

--------------------------------------------------------------------------------

3. You Can’t Weaken Access, But You Can Strengthen It

The rules for access modifiers in overriding are strict but logical: you cannot reduce the scope of the access modifier, but you can increase it.

For instance, if a method in the parent class is declared public, any overriding method in a child class must also be public. You cannot make it protected or default, as that would weaken its accessibility. If you try, the compiler will stop you with a clear error message:

attempting to assign weaker access privileges; was public

However, you are free to increase the scope. A protected method in a parent class can be overridden by a public method in a child class. The hierarchy of access is private -> default -> protected -> public. When overriding, you can only move from left to right in this hierarchy (e.g., protected to public), never from right to left.

Note that private methods are not included here because they are not inherited by child classes and therefore cannot be overridden at all.

This leads us to our final, and perhaps most surprising, rule.

--------------------------------------------------------------------------------

4. You Can Override a Concrete Method with an Abstract One

This last rule is perhaps the most counter-intuitive. It is perfectly valid in Java to override a non-abstract (fully implemented) method from a parent class with an abstract method in the child class.

From a design perspective, this is a powerful tool to control an inheritance hierarchy. But why would you ever want to do this? The primary reason is to force the next level of child classes to provide a completely new implementation. By declaring the method as abstract in the child, you effectively prevent any of its own subclasses (the "grandchildren") from inheriting and using the original implementation from the "grandparent" class.

...we can stop the availability of parent method implementation to the next level child classes.

Of course, the reverse is more common: an abstract method in a parent must be implemented by a child class. The only exception is if the child class is also declared abstract, which effectively pushes the implementation responsibility further down the inheritance chain.

--------------------------------------------------------------------------------

Conclusion

Method overriding is a powerful feature that enables polymorphism and flexible code design in Java. However, its power comes from a set of nuanced rules that go far beyond simply redefining a method. Understanding these details—from runtime method resolution and covariant returns to the strict rules on access modifiers—is a hallmark of a seasoned Java developer.

Which of these overriding rules has surprised you or caused a bug in your own code?

Thursday, December 4, 2025

Beyond extends: 5 Surprising Truths About Java Inheritance

 

Inheritance is one of a Java developer's first lessons. At its core, it's simple: a Child class can inherit fields and methods from a Parent class. This "is-a" relationship is fundamental to object-oriented programming, allowing us to build hierarchies and reuse code. We learn the extends keyword, and for many, the lesson stops there.

But beneath this straightforward definition lies a system of carefully considered rules, counter-intuitive behaviors, and design decisions with profound implications. These rules aren't arbitrary; they exist to ensure type safety, prevent logical contradictions, and enable powerful features like polymorphism.

This article moves beyond the textbook definition to explore some of the most impactful and non-obvious truths about how inheritance really works in Java. Understanding the "why" behind these rules will change how you design and write your code.

A Parent Reference Can't See a Child's Unique Methods—Even When It Holds a Child Object

One of the most flexible features of inheritance is the ability to use a parent class reference to hold a child class object. For example, Parent p1 = new Child(); is perfectly valid Java code. It seems intuitive that since p1 actually holds a Child object, you should be able to call any of the Child's methods. This is not the case.

The rule is that even when a parent reference holds a child object, you can only call methods defined in the parent class using that reference. The instructor in our source material calls this a "very dangerous point" precisely because it feels counter-intuitive. Trying to call a method that only exists in the child class will result in a compiler error, likely stating something like cannot find symbol: method M2() location: class Parent.

Consider this code example:

class Parent {
    public void M1() {
        System.out.println("parent");
    }
}

class Child extends Parent {
    public void M2() {
        System.out.println("child");
    }
}

class Test {
    public static void main(String[] args) {
        Parent p1 = new Child();
        p1.M1(); // Valid
        p1.M2(); // Invalid - Compiler Error!
    }
}

The call to p1.M1() is valid because M1() is defined in the Parent class. However, the call to p1.M2() is invalid because the compiler only sees p1 as a Parent type, and the Parent class has no M2() method.

parent reference can be used to hold child object but by using that reference we can call only the methods available in the parent class we can't call child specific methods

This principle is a cornerstone of Java's compile-time type checking. While it might seem restrictive, it is the key to enabling polymorphism, a powerful concept that allows a single interface to represent different underlying forms.

Code Reusability Isn't Just a Buzzword; It's the Blueprint of the Entire Java API

The primary advantage of inheritance is code reusability. By placing common methods in a parent class, multiple child classes can use them without having to rewrite the same logic, solving the problem of code redundancy. The impact is not trivial; it can dramatically reduce development time and complexity.

Imagine designing a loan module without inheritance. You might have separate classes for different loan types, creating significant code redundancy:

  • VehicleLoan (300 methods)
  • HomeLoan (300 methods)
  • PersonalLoan (300 methods)

This approach requires writing a total of 900 methods and could take 90 hours of development time.

Now, consider the same module built with inheritance. You realize that 250 methods are common to all loan types. By placing these in a parent Loan class, the structure changes:

  • Loan (250 common methods)
  • VehicleLoan extends Loan (50 specific methods)
  • HomeLoan extends Loan (50 specific methods)
  • PersonalLoan extends Loan (50 specific methods)

This new approach requires writing only 400 methods and reduces development time to 40 hours. You write the common code once and reuse it everywhere.

This exact principle is the blueprint for the entire Java API. The Object class is the ultimate parent, or root, for all Java classes. It contains 11 common methods fundamental to any object. Because every class in Java is a child of Object, these 11 methods are automatically available to over 5,000 core Java classes without ever being rewritten.

This design pattern appears in other foundational areas of the API as well. The Throwable class, for instance, acts as the root for the entire Java exception hierarchy. The most common methods required by any exception or error are defined in Throwable, ensuring that this core functionality is available to every class in the hierarchy without being rewritten. This design provides a common, reliable foundation for every object and every throwable in the Java ecosystem.

Java Says "No" to Multiple Inheritance for a Very Good Reason: Ambiguity

A Java class can extend only one other class. An expression like Class A extends B, C is invalid and will cause a compiler error. Java's designers deliberately chose to forbid this kind of "multiple inheritance" for classes to prevent a critical issue: the ambiguity problem.

To understand this, imagine if a class could have two parents.

  • Class C extends both Parent P1 and Parent P2.
  • Parent P1 has a method called M1().
  • Parent P2 also has a method with the exact same name, M1().

Now, if you create an object of Class C and call the M1() method, which version should be executed? The one from P1 or the one from P2? The compiler would have no way to resolve this conflict. This confusion is known as the ambiguity problem. To avoid it entirely, Java simply does not allow a class to extend more than one parent.

...But Interfaces Get a Special Pass to Break the Rule

In surprising contrast to classes, an interface can extend multiple other interfaces. This means interface C extends A, B is perfectly valid. This seems to contradict the rule against multiple inheritance, but it's allowed because the ambiguity problem doesn't apply to interfaces.

The reason is simple: interfaces contain only method declarations (signatures), not implementations (code bodies).

Even if a child interface inherits two M1() method declarations from two different parent interfaces, there is no conflict. The conflict only arises when there are two different implementations to choose from. In the case of interfaces, a single implementation class is responsible for providing the one, unique code body for that method.

even though multiple method declarations are available but implementation is unique and hence there is no chance of ambiguity problem in interfaces

Because the final implementation is singular, there is no ambiguity for the compiler to resolve, making multiple inheritance safe for interfaces.

Here's an expert secret, though: strictly speaking, what interfaces provide isn't true inheritance in the same sense as classes. Because only method signatures are passed down—not implementations—no actual code is being reused. However, because an interface can extend multiple other interfaces, it is commonly and practically referred to as a form of multiple inheritance.

Java Protects You From Creating "Inheritance Loops"

Java's inheritance model requires a clear, hierarchical, and acyclic structure. To enforce this, Java forbids "cyclic inheritance," which would create a logical impossibility.

The following scenarios are illegal and will produce a compiler error for cyclic inheritance involving A:

  1. A class extending itself:
  2. Two classes extending each other:

This is forbidden because, as the source material notes, it's a "useless" concept. If Class A needs all of B's methods and Class B needs all of A's methods, it implies that all the methods should simply be defined within a single, unified class. The prohibition of cyclic inheritance protects the logical integrity of the class hierarchy.

Conclusion: Beyond the Textbook Definition

Inheritance in Java is more than just a simple parent-child relationship for sharing code. It is a robust system governed by a set of well-defined rules that promote code reuse, guarantee type safety, and maintain logical consistency across the entire language. By understanding the reasons behind limitations like the ban on multiple inheritance for classes or the prevention of inheritance loops, we gain a deeper appreciation for the design of the language.

Now that you see the 'why' behind these rules, how does it change the way you think about designing your own class hierarchies?

Featured Post

How LLMs Really Work: The Power of Predicting One Word at a Time

  1.0 Introduction: The Intelligence Illusion The most profound misconception about modern AI is that it understands . While models like Cha...

Popular Posts