Method overriding is the mechanism behind the single most useful sentence in object-oriented Java: "parent reference, child object, and the child's method runs." It's how one line of code can call the right behavior for whatever object it's handed, and it's why interviewers use it to test whether you truly understand runtime polymorphism.
This tutorial covers what makes an override valid, why the JVM — not the compiler — decides which method runs, and the exact rules on access, return types, and exceptions. It's the runtime counterpart to compile-time method overloading, and the two are frequently confused.
What overriding is
Overriding means a subclass provides its own body for a method it inherited, keeping the exact same signature — same name, same parameter list. It requires an inheritance relationship, so it builds directly on inheritance in Java.
class Animal {
void makeSound() {
System.out.println("Some generic sound");
}
}
class Dog extends Animal {
@Override
void makeSound() { // same signature, new body
System.out.println("Woof");
}
}
class Cat extends Animal {
@Override
void makeSound() {
System.out.println("Meow");
}
}
Each subclass replaces the inherited makeSound() with its own version. Nothing surprising yet — the interesting part is what happens when you call it through a parent-typed reference.
Dynamic dispatch: the JVM decides at runtime
Here's the core idea. When you call an overridden method, Java picks the version based on the actual object, not the reference type. This runtime selection is called dynamic method dispatch.
public class Main {
public static void main(String[] args) {
Animal a1 = new Dog(); // parent reference, Dog object
Animal a2 = new Cat(); // parent reference, Cat object
a1.makeSound(); // Woof — Dog's version
a2.makeSound(); // Meow — Cat's version
}
}
Both a1 and a2 are declared as Animal, yet the output differs because the JVM looks at the object each reference points to at runtime. The compiler only checks that Animal has a makeSound() method; the actual choice is deferred to runtime. That deferral is the entire basis of runtime polymorphism, explored fully in the polymorphism tutorial.
Why is this so useful? You can write code against Animal and it correctly handles any future subclass:
static void speak(Animal animal) {
animal.makeSound(); // works for Dog, Cat, or any Animal added later
}
Add a Cow subclass tomorrow and speak needs zero changes. That's the open-closed principle in action.
The @Override annotation: cheap insurance
@Override tells the compiler "I intend to override a parent method — verify it." If your method doesn't actually match a parent method, you get a compile error instead of a silent bug.
class Parent {
void process() { System.out.println("parent"); }
}
class Child extends Parent {
@Override
void proccess() { // typo! COMPILE ERROR because of @Override
System.out.println("child");
}
}
Without @Override, proccess() (misspelled) would compile fine as a brand-new method, and process() would silently keep the parent behavior — a bug that can waste hours. The annotation converts that into an immediate compile error.
Pro tip: Always write
@Override. It costs one line and catches the most common overriding mistake — a signature that doesn't quite match what you think it does. Experienced reviewers treat a missing@Overrideon an intended override as a code smell.
The rules that make an override valid
An override must obey a specific set of rules. Interviewers love turning each into a question.
Signature: Same method name and same parameter list. Different parameters means you've written an overload, not an override.
Return type: Must be the same, or a subtype of the parent's return type (a covariant return, added in Java 5).
class AnimalShelter {
Animal adopt() { return new Animal(); }
}
class DogShelter extends AnimalShelter {
@Override
Dog adopt() { return new Dog(); } // Dog is a subtype of Animal: legal
}
Returning Dog where the parent returned Animal is fine because any caller expecting an Animal is happy to receive a Dog.
Access modifier: Cannot be more restrictive. You may widen protected to public, never narrow public to protected.
class Base {
public void show() {}
}
class Derived extends Base {
// protected void show() {} // ERROR: cannot reduce visibility
@Override public void show() {} // OK: same or wider
}
Narrowing would break polymorphism — a caller using the Base reference expects show() to be callable, so the override can't hide it.
Exceptions: An overriding method cannot throw new or broader checked exceptions than the parent declared. It may throw fewer, narrower, or unchecked exceptions.
What you cannot override
Three kinds of methods can't be overridden, and each behaves differently:
finalmethods — thefinalkeyword explicitly forbids overriding. Attempting it is a compile error.staticmethods — redefining a static method in a subclass is method hiding, not overriding.privatemethods — invisible to the subclass, so a same-named method there is simply a new, unrelated method.
The static case is the classic trap:
class Base {
static void greet() { System.out.println("Base"); }
}
class Derived extends Base {
static void greet() { System.out.println("Derived"); }
}
public class Main {
public static void main(String[] args) {
Base ref = new Derived();
ref.greet(); // prints "Base" — resolved by reference type!
}
}
Contrast this with the instance-method example earlier: there the object decided, here the reference type decides. That's because static methods are bound at compile time. This exact snippet is a favorite interview question.
Interview note: "Is method hiding the same as overriding?" — no. Overriding is resolved by the object at runtime (dynamic); hiding a static method is resolved by the reference type at compile time (static). If you can explain why
ref.greet()above prints "Base," you've demonstrated real understanding. More traps like this are on our Java OOP interview questions page.
Calling the parent version with super
An override doesn't have to discard the parent behavior entirely — it can extend it by calling super.method().
class Logger {
void log(String msg) {
System.out.println("[LOG] " + msg);
}
}
class TimestampLogger extends Logger {
@Override
void log(String msg) {
super.log(System.currentTimeMillis() + " " + msg); // reuse + extend
}
}
super.log(...) runs the parent's version, letting TimestampLogger add a timestamp and then delegate the actual printing. This "override to add, not replace" pattern is extremely common in real frameworks.
Overriding vs overloading, one more time
Because these two dominate fresher interviews, keep the contrast sharp: overriding is same signature, across parent and child, resolved at runtime by the object. Overloading is same name with different parameters, in one class, resolved at compile time by the compiler. If someone asks "which is runtime polymorphism?" the answer is overriding. The full comparison table lives in the method overloading tutorial.
Where to go next
Overriding is the engine; polymorphism in Java is the design payoff, so read that next to see why writing code against parent types makes systems extensible. Revisit inheritance in Java if the super and constructor-chaining details feel shaky, since overriding depends on that hierarchy.
To get comfortable predicting output for dynamic-dispatch and method-hiding questions the way interviewers pose them, the OOP module of the Java Full Stack course drills them with a mentor reviewing your reasoning, and the Java learning hub shows the full path from here.
Frequently Asked Questions
What is method overriding in Java?
What does the @Override annotation do?
Can you override static methods in Java?
What is a covariant return type in overriding?
Can an overriding method have a more restrictive access modifier?
Want to Build Your Career in Java Full Stack with AI?
Join CodeBegun and train with working industry engineers — Explore the Program

