Topic 62 of 119 6. Object-Oriented Programming in Java Interactive Tutorial

Java Method Overriding: Runtime Polymorphism and @Override

18 mins read Last updated: 22 Aug 2026 30,412 views

Direct Answer & Core Concept

A method is an override when a subclass supplies a new implementation for an inherited instance method. The signature must match exactly, and the compiler will not forgive a near miss.
Main.java
Run Online
public class EmailNotification extends Notification {
    @Override public void send(String message) {
        System.out.println("email: " + message);
    }
}
Four things must hold, and each is checked at compile time:
  • Signature. Same name, same parameter types, same arity. Return type must be identical or covariant, meaning narrower.
  • Visibility. The override may widen access but never narrow it, so a public method cannot become protected.
  • Checked exceptions. The override may throw a subset, never a superset.
  • Kind of method. Only instance methods qualify. static methods are hidden, private ones are invisible, and final ones are locked.
The rule that causes the most silent bugs is the first one. Change a parameter type, or reorder two parameters, and the compiler treats it as a new method plus overload, not an override. The parent version stays reachable, so calling through a parent reference silently runs the parent behaviour. That is precisely what @Override exists to catch:
Main.java
Run Online
@Overridepublic void send(StringBuilder message) {
} // COMPILE ERROR
Without the annotation this compiles into two unrelated methods. With it, the mismatch becomes a build failure. The annotation has no effect on runtime behaviour at all; it is purely a compile-time guard, and it is worth adding to every override. One more subtlety: an override is chosen by the actual object type, but the compiler will only let you call it through the supertype. That is why a downcast is required to reach a child-only method, and why dispatch already happens without one.

Detailed Explanation & Mechanics

1) What Counts as an Override

An override replaces one specific inherited method. Everything else you write in a subclass is something else entirely: an overload, a hiding method, or a new method.

Change in the subclassWhat it isDispatched
Identical signatureOverrideYes, on the child
Different parameter typesOverloadNo, both exist
Same name, fewer parametersOverloadNo, both exist
Same name, reordered parametersOverloadNo, both exist
Same signature, staticHidingNo, bound statically
New name entirelyNew methodNo

Only the first row is an override. The rest compile without complaint, which is why the @Override annotation matters so much in practice.

2) Overload or Override

This is the most common confusion, and the test is simple: does the parameter list change? If it does, you have written an overload.

Main.java
Run Online
public class Notification {
    public void send(String message) {
        System.out.println("standard: " + message);
    }
}public class EmailNotification extends Notification {
    @Override public void send(String message) {
        // an OVERRIDE
        System.out.println("email: " + message);
    } public void send(String message, boolean urgent) {
        // an OVERLOAD
        System.out.println("urgent email: " + message);
    } public void send(StringBuilder message) {
        // AN OVERLOAD, NOT A TYPO FIX
        System.out.println("built: " + message);
    }
}

The third method is the trap. It looks like a correction to a wrong type, but because StringBuilder differs from String, it is an additional overload and the inherited send(String) is left untouched. The overloading topic covers the resolution rules.

3) Covariant Return Types

The return type need not be identical, but it must be identical or narrower. Returning a subtype is legal and is called covariance.

Main.java
Run Online
public class Shape {
    public Object describe() {
        return "a shape";
    }
}public class Circle extends Shape {
    @Override public String describe() {
        // covariant, String is a subtype of Object
        return "a circle";
    }
}

Going the other way is rejected, because a caller holding the result as an Object could not cope with a String.

Main.java
Run Online
public class Square extends Shape {
    @Override public Object describe() {
        // COMPILE ERROR, this widens the return type
        return "a square";
    }
}

Because the declared return type of the reference governs, a caller using a Shape variable still receives an Object even when the object is a Circle. Covariance tightens the contract for the subclass, it does not change what earlier code sees.

4) Access May Widen but Never Narrow

An override cannot reduce visibility, because a caller holding a supertype reference must still be able to call the method.

Main.java
Run Online
public class Base {
    public void run() {
    }
}public class Derived extends Base {
    @Override protected void run() {
        // COMPILE ERROR, weaker access
    } @Override public void run() {
        // legal, same access
    }
}public class Other extends Base {
    @Override public void run() {
        // legal, stronger access
    }
}

This rule guarantees substitutability. A subclass that hid a public method behind protected would break any code written against the parent type.

Because the access modifiers ladder is ordered, you can widen from protected to public and from package-private to either, but you can never move down the ladder.

5) Checked Exceptions May Narrow but Never Widen

An override may throw fewer exceptions, or a subset, but it may not introduce a new checked exception the parent never declared.

Main.java
Run Online
import java.io.IOException;
public class Reader {
    public void read() throws IOException {
    }
}public class FileReader extends Reader {
    @Override public void read() {
        // legal, declares fewer
    } @Override public void read() throws IOException {
        // legal, same
    } @Override public void read() throws InterruptedException {
        // COMPILE ERROR
    }
}

The reasoning mirrors access widening: code calling through the parent type must already handle what the parent promised, so a subclass cannot add a new obligation.

Unchecked exceptions such as RuntimeException are exempt, since callers are not required to catch them anyway.

Main.java
Run Online
public class Lenient extends Reader {
    @Override public void read() {
        throw new IllegalStateException("unchecked, so this is fine");
    }
}

6) Methods That Cannot Be Overridden

Three categories are excluded, each for a different reason.

Main.java
Run Online
public class Base {
    private void hidden() {
        System.out.println("private");
    } public static void statics() {
        System.out.println("static");
    } public final void locked() {
        System.out.println("final");
    } public void visible() {
    }
}
KindWhySubclass effect
privateNot visible to the subclassNew, unrelated method
staticBelongs to the class, not the objectHidden, bound statically
finalLocked by the parentCompile error
Package-private, different packageInvisible across packagesNew, unrelated method

A package-private method is invisible to a subclass in a different package, so writing a same-named method there creates a new method rather than an override. This one compiles, dispatches to the parent implementation, and produces no warning of any kind.

7) Why the Annotation Is Not Optional in Practice

@Override does nothing at runtime. It exists so the compiler verifies that the method really overrides something, and its value shows up the moment a signature drifts.

Main.java
Run Online
public class Report extends Base {
    @Override public void render(StringBuilder out) {
        // COMPILE ERROR, no such parent method
    }
}
Main.java
Run Online
public class Report extends Base {
    public void render(StringBuilder out) {
        // compiles, and silently does nothing useful
    }
}

In the second version the parent method still exists and still runs for any caller holding a Base reference. The subclass looks like it specialises the behaviour and does not specialise it at all.

The annotation also catches a rename in the parent. If the parent method is later renamed, every annotated override fails to compile, whereas unannotated ones quietly become new methods and the polymorphism disappears without warning.


Diagram showing the five checks a compiler applies before accepting a method as an override
Every rule is checked at compile time; nothing about an override is decided at runtime.

8) Overriding toString, equals, and hashCode

These three are inherited from Object and are the most commonly overridden methods in real code. Each carries a contract beyond the usual signature rules.

Main.java
Run Online
public class Point {
    private final int x;
    private final int y;
    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    } @Override public String toString() {
        return "Point(" + x + ", " + y + ")";
    } @Override public boolean equals(Object other) {
        if (this == other) {
            return true;
        } if (!(other instanceof Point)) {
            return false;
        } Point that =(Point) other;
        return this.x == that.x && this.y == that.y;
    } @Override public int hashCode() {
        return java.util.Objects.hash(x, y);
    }
}
MethodContractBreaking it causes
toStringReadable descriptionUnreadable output
equalsReflexive, symmetric, transitiveBroken collections
hashCodeEqual objects share a hashMissing entries in sets

Overriding equals without hashCode is the classic mistake. Two equal points can then land in different hash buckets, so a HashSet or HashMap will hold both of them. The collections topics cover why the two must agree.

9) Calling the Parent Version

An override replaces the parent body entirely. To reuse it, call super explicitly rather than copying code.

Main.java
Run Online
public class PremiumNotification extends EmailNotification {
    @Override public void send(String message) {
        super.send(message.toUpperCase());
        System.out.println("and it is premium");
    }
}

The super keyword topic covers this in detail. Note that super invokes the parent implementation directly, bypassing dispatch, so no infinite recursion occurs.

10) Overriding Across a Hierarchy

Overrides chain. If three levels each override the same method, the most derived version runs, and the others are reachable only through super.

Main.java
Run Online
public class A {
    public void show() {
        System.out.println("A");
    }
}public class B extends A {
    @Override public void show() {
        System.out.println("B");
    }
}public class C extends B {
    @Override public void show() {
        super.show();
        // prints B
        System.out.println("C");
    }
}
Main.java
Run Online
A reference = new C();
reference.show();
// prints B then C, never A

An abstract class may also declare an abstract method that forces every concrete subclass to provide it, which makes overriding mandatory rather than optional.

When a subclass overrides an abstract method, the @Override annotation is optional but still recommended. It documents the intent and catches a rename in the abstract declaration.

11) Common Misconceptions

ClaimVerdict
Return type must match exactlyFalse. Covariant returns are allowed.
Access may be narrowedFalse. Widening only.
Checked exceptions may be addedFalse. Narrowing only.
Static methods are overriddenFalse. They are hidden.
Private methods are overriddenFalse. They are invisible.
@Override changes behaviourFalse. It is compile-time only.
A renamed parameter type still overridesFalse. It becomes an overload.
Overriding needs a castFalse. The JVM dispatches for you.

Frequently Asked Questions

What makes a method an override rather than an overload?


Whether the parameter list is unchanged. An override keeps the same name, parameter types, and arity, while an overload changes the parameters and leaves the inherited method in place. Changing a parameter type or reordering parameters produces an overload, even when it looks like a correction of a typo.

Can an overriding method return a different type?


Yes, as long as the type is a subtype of the declared return type, which is called a covariant return. The reverse is rejected, because callers compiled against the parent return type could not handle a wider result. Covariance tightens the contract without changing what existing code sees.

Can an override reduce visibility?


No. An override may widen access but never narrow it, so a public parent method cannot become protected in the child. This preserves substitutability, since code holding a supertype reference must still be able to call the method it was promised.

What exceptions may an override throw?


Only a subset of what the parent declared, and never a new checked exception, since callers of the parent type have not agreed to handle it. Unchecked exceptions are exempt because callers were never required to catch them. Narrowing in this way is safe because the parent contract is always honoured.

Which methods can never be overridden?


Static methods, because they belong to the class rather than the object, so a same-named method hides rather than overrides them. Private methods, because a subclass cannot see them. Final methods, because the parent forbids it. A package-private method also cannot be overridden from a different package, since it is invisible there.

What does the @Override annotation actually do?


Nothing at runtime. It instructs the compiler to verify that the method really overrides an inherited one, so a mismatched signature becomes a build failure rather than a new method that silently does nothing useful. It is not mandatory, but it is the cheapest guard against a signature drifting out of sync with the parent.

Do I need a downcast to reach an override?


No. When a method is called through a supertype reference, the JVM already selects the override belonging to the actual object. A cast is needed only to access a method that exists on the child but not on the parent, which is a separate concern from dispatch.

Why must equals and hashCode be overridden together?


Because the contract requires that equal objects produce equal hash codes. Overriding equals alone leaves two equal objects in different hash buckets, so a HashSet can contain both and a HashMap lookup can miss an entry that is present. Overriding both keeps the two consistent.

Runnable Code Example: Java Method Overriding

Java 17+
Main.java
Run Online
// Shows which signatures the compiler accepts as overrides,
// which it silently treats as overloads, and the methods it refuses.
public class MethodOverridingDemo {
    private static final String MESSAGE = "order shipped";
    private static final String TITLE = "report";
    private static final int X = 3;
    private static final int Y = 4;
    private static final String BUILT = "built: ";
    // THE PARENT, ITS CONTRACT IS WHAT EVERY CHILD MUST HONOUR
    public static class Notification {
        public String channel() {
            return "standard";
        } public void send(String message) {
            System.out.println("standard send: " + message);
        } public void send(String message, boolean urgent) {
            System.out.println("standard urgent send: " + message);
        } private void hidden() {
            System.out.println("parent private");
        } public static void announce() {
            System.out.println("parent static");
        } public final void locked() {
            System.out.println("parent final");
        }
    } // A GENUINE OVERRIDE, IDENTICAL SIGNATURE PLUS A COVARIANT RETURN
    public static class EmailNotification extends Notification {
        @Override public String channel() {
            return "email";
        } @Override public void send(String message) {
            System.out.println("email send: " + message);
        } // NOT AN OVERRIDE, THE PARAMETER LIST CHANGED, SO IT OVERLOADS
        public void send(String message, boolean urgent) {
            super.send(message, urgent);
            System.out.println("email urgent send");
        } // NOT AN OVERRIDE EITHER, A StringBuilder IS NOT A String
        public void send(StringBuilder message) {
            System.out.println("built " + message);
        } // A SAME-NAMED PRIVATE METHOD IS A NEW METHOD, NOT AN OVERRIDE
        private void hidden() {
            System.out.println("child private, unrelated to the parent one");
        } // A STATIC METHOD WITH THE SAME NAME IS HIDDEN, NOT OVERRIDDEN
        public static void announce() {
            System.out.println("email static");
        }
    } // ACCESS MAY WIDEN, AND THIS CHILD USES super TO REUSE THE PARENT BODY
    public static class PremiumNotification extends EmailNotification {
        @Override public void send(String message) {
            super.send(message);
            System.out.println("premium add-on applied");
        }
    } // OVERRIDES CHAIN, AND ONLY super REACHES THE LEVELS ABOVE
    public static class LevelOne extends Notification {
        @Override public void send(String message) {
            System.out.println("level one");
        }
    } public static class LevelTwo extends LevelOne {
        @Override public void send(String message) {
            super.send(message);
            System.out.println("level two");
        }
    } // OVERRIDING THE THREE Object METHODS, AND PAIRING equals WITH hashCode
    public static class Point {
        private final int x;
        private final int y;
        public Point(int x, int y) {
            this.x = x;
            this.y = y;
        } public int getX() {
            return x;
        } public int getY() {
            return y;
        } @Override public String toString() {
            return "Point(" + x + ", " + y + ")";
        } @Override public boolean equals(Object other) {
            if (this == other) {
                return true;
            } if (!(other instanceof Point)) {
                return false;
            } Point that =(Point) other;
            return this.x == that.x && this.y == that.y;
        } @Override public int hashCode() {
            return java.util.Objects.hash(x, y);
        }
    } public static void main(String[] args) {
        // THE JVM PICKS THE OVERRIDE FROM THE ACTUAL OBJECT
        Notification reference = new EmailNotification();
        System.out.println("channel: " + reference.channel());
        reference.send(MESSAGE);
        // A GRANDCHILD OVERRIDE STILL DISPATCHES, AND super REACHES UP
        Notification premium = new PremiumNotification();
        premium.send(MESSAGE);
        System.out.println();
        System.out.println("overrides chain:");
        Notification deep = new LevelTwo();
        deep.send(MESSAGE);
        // AN OVERLOAD IS CHOSEN BY THE COMPILER FROM DECLARED TYPES
        System.out.println();
        System.out.println("overloads:");
        EmailNotification email = new EmailNotification();
        email.send(MESSAGE, true);
        email.send(new StringBuilder(BUILT + MESSAGE));
        // STATIC METHODS BIND TO THE REFERENCE TYPE, SO NO DISPATCH
        System.out.println();
        System.out.println("statics:");
        Notification.announce();
        // parent, chosen by the declared type
        EmailNotification.announce();
        // child, called explicitly by name
        // equals AND hashCode MUST AGREE, OR SETS KEEP DUPLICATES
        System.out.println();
        System.out.println("equals and hashCode:");
        System.out.println("title: " + TITLE);
        Point first = new Point(X, Y);
        Point second = new Point(X, Y);
        System.out.println("first:   " + first);
        System.out.println("second:  " + second);
        System.out.println("equal:   " + first.equals(second));
        System.out.println("hashes:  " +(first.hashCode() == second.hashCode()));
        java.util.Set<Point> unique = new java.util.HashSet<>();
        unique.add(first);
        unique.add(second);
        System.out.println("set size: " + unique.size());
        /*
            ACTUAL OUTPUT:

                channel: email
                email send: order shipped

            overrides chain:
                level one
                level two

            overloads:
                standard urgent send: order shipped
                email urgent send
                built built: order shipped

            statics:
                parent static
                email static

            equals and hashCode:
                title: report
                first:   Point(3, 4)
                second:  Point(3, 4)
                equal:   true
                hashes:  true
                set size: 1

            THE SEVEN RULES THAT MATTER MOST
            1) AN OVERRIDE KEEPS THE PARAMETER LIST EXACTLY. Change a
               parameter type or reorder them and the compiler sees a
               new overload, leaving the inherited method reachable.
            2) THE RETURN TYPE MAY NARROW BUT NEVER WIDEN. A covariant
               subtype is allowed, which tightens the contract for the
               child without changing what earlier callers see.
            3) ACCESS MAY WIDEN AND NEVER NARROW. A public parent
               method cannot become protected, because code holding
               a supertype reference must still reach it.
            4) CHECKED EXCEPTIONS MAY NARROW AND NEVER WIDEN. Callers
               of the parent never agreed to handle anything extra.
            5) STATIC, PRIVATE, AND FINAL METHODS ARE EXCLUDED. Static
               ones are hidden, private ones are invisible, and final
               ones are locked by the parent.
            6) ALWAYS WRITE @Override. It does nothing at runtime, but
               it turns a silent mismatch into a compile error, which
               is the entire point of it.
            7) equals AND hashCode TRAVEL TOGETHER. Overriding equals
               alone leaves equal objects in different hash buckets,
               so the set above holds two copies of one value.
        */
    }
}

Common Java Technical Interview Questions & Answers

Interview Prep
Q1: Can private, static, or final methods be overridden in Java?\nA1: No. A private method is invisible to the subclass, so a same-named method there is an unrelated new method. A static method belongs to the class rather than the object, so it is hidden and bound by the reference type. A final method is locked by the parent, and an attempt to override it is a compile error.\nQ2: What is a covariant return type in method overriding?\nA2: It is a return type in the child that is a subtype of the one declared in the parent. Java permits this so a subclass can express a more specific result, and it is safe because every caller holding the parent return type can already handle the narrower one. Widening the other way is rejected.\nQ3: Why can an override not reduce visibility?\nA3: Because code holding a supertype reference must remain able to call the method it was promised. Narrowing a public parent method to protected in the child would break every such caller at compile time, so an override may widen access but never reduce it.\nQ4: What exceptions is an override allowed to throw?\nA4: Only a subset of the checked exceptions the parent declared, and never a new one, since callers compiled against the parent never agreed to handle it. Unchecked exceptions such as RuntimeException are exempt because callers were never required to catch them. Narrowing is safe because the parent contract is always satisfied.\nQ5: What does the @Override annotation actually do?\nA5: Nothing at runtime. It tells the compiler to verify that the method really overrides an inherited one, so a mismatched signature becomes a build failure instead of a new method that silently leaves the parent implementation reachable. It is optional, but it is the cheapest guard against signatures drifting apart.\nQ6: How do I tell an override from an overload?\nA6: Look at the parameter list. An override keeps the same name, parameter types, and arity. An overload changes the parameters, so the inherited method survives alongside it. Changing a parameter type or reordering parameters produces an overload even when it looks like a correction of a typo.\nQ7: Do I need to cast to reach an override?\nA7: No. When a method is invoked through a supertype reference, the JVM already selects the implementation belonging to the actual object. A cast is required only to access a method declared on the child but not on the parent, which is unrelated to how dispatch works.\nQ8: Why must equals and hashCode be overridden together?\nA8: Because the contract requires that equal objects produce equal hash codes. Overriding equals alone lets two equal objects land in different hash buckets, so a HashSet can hold both and a HashMap lookup can miss an entry that is present. Overriding both keeps the two behaviours consistent.\nQ9: Can an override call the parent implementation?\nA9: Yes, by invoking super with the same signature. This calls the parent version directly rather than dispatching, so there is no risk of infinite recursion. It is the right way to extend behaviour rather than duplicating the parent body in the child.

Key Takeaways, Best Practices & Clean Code Rules

Java Best Practice & Production Rules

Add the @Override annotation to every overriding method without exception.\nNever reduce visibility when overriding, keep the contract at least as wide.\nNarrow checked exceptions rather than adding new ones.\nReturn a covariant type when it genuinely describes the subclass better.\nOverride toString, equals, and hashCode as a set, never equals alone.\nCall super rather than copying the parent body into the child.\nDo not rely on overriding for static methods, since they bind statically.\nRemember a renamed parameter type silently becomes an overload.\nKeep the override consistent with the parent contract in behaviour.\nPrefer a public method over protected so callers are not restricted.\nDocument why an override behaves differently from the parent.\nTest through a supertype reference to confirm dispatch actually works.\nAvoid calling overridable methods from constructors, which run too early.\nUse pattern matching for instanceof so casts stay readable.\nKeep equals reflexive, symmetric, and transitive, since sets depend on it.\nFavour composition when you only want to vary behaviour, not type.
We value your privacy & transparency

OperateTools uses cookies and local storage to ensure basic tool functionality, preserve preferences, and analyze aggregated traffic. We never sell your personal data or index your document files. Learn more in our Privacy Policy.