Back to Blog
Java

Java Method Overriding: Rules and Runtime Behavior

Understand the mechanics of java method overriding, including the rules for signatures, access modifiers, exceptions, and how the JVM selects the correct method at run...

Method OverridingPolymorphismJava InheritanceAnnotationsJava Virtual Machine
A Java method overriding concept with a subclass method symbolically replacing a superclass method.

When you declare a method in a subclass that has the same signature as a method in its superclass, you are performing java method overriding. The JVM decides which version of the method to call based on the actual runtime type of the object, not the compile-time reference type. This is dynamic dispatch, the foundation of runtime polymorphism in Java.

Consider a simple hierarchy with a bank account and its specialized subclasses. A base class may define getInterestRate(), returning a default rate, while a SavingsAccount overrides it to return a higher rate. When you call the method through a reference of the base type, the JVM examines the actual object's class and invokes the overriding version.

class BankAccount { public double getInterestRate() { return 0.02; } } class SavingsAccount extends BankAccount { @Override public double getInterestRate() { return 0.04; } } public class Main { public static void main(String[] args) { BankAccount account = new SavingsAccount(); System.out.println(account.getInterestRate()); // prints 0.04 } }

The @Override annotation is not required, but it is strongly recommended. It lets the compiler check that the method actually overrides a superclass method. If you misspell the method name or change the parameter list, the compiler fails with an error instead of silently creating a new method.

Method Signature Rules and Covariant Return Types

For a method to override another, the signature must match the superclass method's name and parameter types. The parameter order and types must be identical. Changing the parameter list creates an overloaded method, not an override. This distinction is a common source of confusion and subtle bugs.

Java 5 introduced covariant return types, which allow an overriding method to return a subtype of the original return type. This makes the overriding method more specific without violating substitutability. For example, a clone() method in a subclass can return the subclass type instead of Object.

class Animal { Animal reproduce() { return new Animal(); } } class Dog extends Animal { @Override Dog reproduce() { return new Dog(); } }

While the return type can narrow, the overriding method must not widen the return type. The imposed rule is that the new return type must be a subtype of the original, or the same type. This rule preserves type safety when calling through the base reference.

Access Modifier Restrictions

The overriding method cannot reduce visibility. If the superclass method is public, the override must be public. If it is protected, the override can be protected or public, but never private or package-private. Reducing access would violate the expectation that callers using the base type can invoke the method.

Superclass AccessAllowed Override Access
publicpublic
protectedprotected or public
package-privatepackage-private, protected, or public

Increasing visibility is always allowed. This rule ensures that any code that works with the base type continues to work with the subclass. A private method in the superclass is never overridden; it is hidden in the subclass, and the dispatch works differently.

Exception Handling in Overridden Methods

The overriding method cannot throw broader checked exceptions than the original method. It may throw the same checked exceptions, subclasses of those exceptions, or no checked exceptions at all. It may also add new unchecked exceptions, because unchecked exceptions do not appear in the method signature contract.

This restriction prevents callers from being forced to handle exceptions they did not expect when programming against the base type. The following example demonstrates valid exception narrowing:

class Parent { void process() throws IOException { // ... } } class Child extends Parent { @Override void process() throws FileNotFoundException { // ... } }

Attempting to throw a broader checked exception, such as Exception, causes a compile error. Unchecked exceptions like IllegalArgumentException are always permitted.

Static Methods and Fields Cannot Be Overridden

Static methods belong to the class, not to instances. Declaring a static method with the same signature in a subclass hides the superclass method rather than overriding it. The JVM resolves static methods at compile time based on the reference type.

Similarly, fields are not polymorphic. Accessing a field through a reference uses the reference type, not the runtime object type. This often leads to surprising output, especially when fields are shadowed in subclasses.

class Parent { static String name = "Parent"; } class Child extends Parent { static String name = "Child"; } public class Main { public static void main(String[] args) { Parent p = new Child(); System.out.println(p.name); // prints Parent Child c = new Child(); System.out.println(c.name); // prints Child } }

Use polymorphism through methods instead of direct field access. Encapsulation is maintained, and the behavior is predictable.

The Role of Dynamic Method Dispatch

The JVM uses the actual class of the object to select the method to execute. This process is known as dynamic method dispatch or virtual method invocation. During bytecode execution, the invoked method is resolved through the object's class hierarchy.

Each class in HotSpot has a virtual method table that holds pointers to the actual bytecode for each overridable method. When a method is called, the JVM reads the table of the actual class to find the correct implementation. This process adds a small pointer lookup compared to static resolution, but it is optimized by modern JIT compilers.

Dynamic dispatch is what enables frameworks and libraries to extend behavior through user-defined subclasses. For example, a collection framework might call an overridden equals() implemented by user code, allowing the framework to use custom equality logic.

Constructor and Initialization Order in Overriding

An overridden method can behave unexpectedly during object construction if it accesses fields not yet initialized. When a subclass constructor calls super(), the superclass constructor runs before the subclass fields are set. If the superclass constructor invokes an overridable method, the overriding version in the subclass runs while the subclass fields still have their default values.

class Base { Base() { init(); } void init() { System.out.println("Base init"); } } class Derived extends Base { private String label = "initial"; @Override void init() { System.out.println(label); // prints null } } public class Main { public static void main(String[] args) { new Derived(); } }

This prints null because the label field has not been assigned yet. The solution is to avoid calling overridable methods from constructors. Non-final methods are subject to virtual dispatch, and the subclass implementation may rely on state that does not exist yet.

Final Methods and Classes Cannot Be Overridden

A final method cannot be overridden by any subclass. This guarantees that the implementation stays consistent for entire inheritance hierarchies. It also lets the compiler perform optimizations that are impossible when a method could be redefined.

A final class cannot be subclassed at all, so none of its methods can be overridden. This is useful for security and immutability, such as String and Integer. When writing a library, marking a method final signals that the behavior is a fixed contract and should not be changed.

If you attempt to override a final method, the compiler reports an error. If you do not intend a method to be overridden, but other developers subclass your class, applying final prevents accidental and intentional redefinitions.

Overriding versus Overloading

Overloading occurs when multiple methods share the same name but differ in parameter list. Overloading is resolved at compile time based on the method signature. Overriding is resolved at runtime based on the object type.

AspectOverridingOverloading
Method nameSameSame
ParametersSame signaturesDifferent parameter lists
ResolutionRuntime (dynamic dispatch)Compile time
RelationshipBetween superclass and subclassWithin the same class
Keywords@Override optionalNo annotation

Both features can appear together, but they serve different purposes. Overriding enables polymorphic behavior, while overloading provides convenience with different argument combinations.

java method overriding: Practical Usage and Code Examples | RYUSLOG DEV