Method Override
Method overriding is when a subclass provides its own implementation of a method it inherited from a superclass (or of a method declared in an interface it implements). When the method is called on an object of the subclass, the subclass version runs, even if the call is made through a variable of the superclass type. Overriding is the mechanism behind runtime polymorphism.
Basic Example
public class Animal {
public String sound() {
return "...";
}
public void speak() {
System.out.println(getClass().getSimpleName() + " says " + sound());
}
}
public class Dog extends Animal {
@Override
public String sound() {
return "Woof";
}
}
public class Cat extends Animal {
@Override
public String sound() {
return "Meow";
}
}
List<Animal> animals = List.of(new Animal(), new Dog(), new Cat());
for (Animal a : animals) {
a.speak();
}
Output:
Animal says ...
Dog says Woof
Cat says Meow
speak() is defined only in Animal. Inside it, the call to sound() runs the version belonging to the actual object: Dog's for a dog, Cat's for a cat. The parent class writes the general behavior once, and subclasses customize only the part that differs.
How the JVM Chooses: Dynamic Dispatch
Animal a = new Dog();
a.sound(); // "Woof"
The variable's declared type is Animal, but the object is a Dog. For overridden instance methods, the JVM looks at the object's class at runtime and runs the most specific implementation available, searching upward through the hierarchy until it finds one. This is called dynamic dispatch.
The declared type still matters at compile time: it decides which methods may be called at all. a.fetch() would not compile even if Dog defined fetch(), because not every Animal has it.
Rules for a Valid Override
An overriding method must have:
| Rule | Detail |
|---|---|
| Same name and parameter types | Otherwise it is an overload, not an override |
| Compatible return type | The same type, or a subtype (a covariant return) for reference types; primitives must match exactly |
| Same or wider access | protected may become public; public may not become protected |
| No new or broader checked exceptions | It may throw fewer, narrower, or none |
And the parent method must be:
- Visible to the subclass (not
private) - Not
final - Not
static(static methods are hidden, not overridden)
Covariant return types
An override may return a more specific type than the original:
class Shape {
Shape copy() { return new Shape(); }
}
class Circle extends Shape {
@Override
Circle copy() { return new Circle(); } // Circle is a subtype of Shape: allowed
}
Circle c = new Circle().copy(); // no cast needed
Access cannot be narrowed
class Base {
public void run() { }
}
class Derived extends Base {
@Override
protected void run() { } // compile error: attempting to assign weaker access privileges
}
Code holding a Base reference may call run() from anywhere. If a subclass could make it protected, some objects would reject a call the compiler had already approved.
Checked exceptions cannot be broadened
class Reader {
String read() throws IOException { ... }
}
class SafeReader extends Reader {
@Override
String read() { ... } // allowed: throws nothing
// String read() throws Exception { ... } // compile error: Exception is broader than IOException
}
Callers of Reader.read() are prepared to handle IOException. An override that could throw some other checked exception would surprise them. Unchecked exceptions (RuntimeException and its subclasses) are not restricted by this rule.
The @Override Annotation
@Override tells the compiler: "this method is meant to override a method from a supertype." The compiler then verifies it. Without the annotation, small mistakes silently create a new method instead:
class Product {
final String sku;
Product(String sku) { this.sku = sku; }
public boolean equals(Product other) { // intended to override equals(Object)
return sku.equals(other.sku);
}
}
The parameter is Product, not Object, so this is an overload. Collections and Objects.equals call equals(Object), the inherited version that compares identity, and two products with the same SKU are treated as different. Adding @Override turns the bug into a compile error:
error: method does not override or implement a method from a supertype
Other mistakes @Override catches: misspelled names (tostring, hashcode), wrong parameter types, and a parent method that was renamed or removed later. Use it on every method intended to override.
Calling the Parent Version: super.method()
An override can call the method it replaces using super:
public class Account {
protected double balance;
public void withdraw(double amount) {
if (amount > balance) throw new IllegalStateException("Insufficient funds");
balance -= amount;
}
}
public class FeeAccount extends Account {
private static final double FEE = 10;
@Override
public void withdraw(double amount) {
super.withdraw(amount + FEE); // reuse the parent's checks and logic
}
}
The override adds a fee but keeps the parent's insufficient-funds check. Extending the parent's behavior through super is usually safer than copying its code, which would drift out of sync when the parent changes.
Overriding Methods of Object
Every class inherits methods from java.lang.Object. Three are commonly overridden.
toString
public class Book {
private final String title;
private final String author;
public Book(String title, String author) {
this.title = title;
this.author = author;
}
@Override
public String toString() {
return title + " by " + author;
}
}
System.out.println(new Book("Clean Code", "Robert C. Martin"));
// Clean Code by Robert C. Martin
Without the override, the output would be something like Book@4e25154f.
equals and hashCode
equals defines when two objects are logically equal. hashCode must be consistent with it: equal objects must have equal hash codes. Override both or neither.
import java.util.Objects;
public class Book {
private final String isbn;
private final String title;
public Book(String isbn, String title) {
this.isbn = isbn;
this.title = title;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Book other)) return false;
return isbn.equals(other.isbn); // books are equal if their ISBNs match
}
@Override
public int hashCode() {
return Objects.hash(isbn); // uses the same field as equals
}
}
Set<Book> shelf = new HashSet<>();
shelf.add(new Book("978-0132350884", "Clean Code"));
shelf.add(new Book("978-0132350884", "Clean Code (2nd printing)"));
System.out.println(shelf.size()); // 1: recognized as the same book
If hashCode were not overridden, the two objects would almost certainly have different hash codes, HashSet would place them in different buckets, and the set would contain two "equal" books. Records generate correct equals, hashCode, and toString automatically.
What Cannot Be Overridden
| Member | Why | What happens instead |
|---|---|---|
private methods | Not visible to subclasses | A same-named subclass method is an unrelated new method |
static methods | Belong to the class, resolved at compile time | A same-signature subclass method hides the parent's |
final methods | Explicitly locked | Compile error |
| Constructors | Not inherited | Each class declares its own |
| Fields | Not polymorphic | A same-named field hides the parent's |
class Parent {
static String id() { return "parent"; }
}
class Child extends Parent {
static String id() { return "child"; } // hides, does not override
}
Parent p = new Child();
System.out.println(p.id()); // "parent": static methods use the declared type
Overriding Interface and Abstract Methods
Implementing an abstract method or an interface method follows the same rules and also uses @Override:
public interface Greeter {
String greet(String name);
}
public class FormalGreeter implements Greeter {
@Override
public String greet(String name) {
return "Good morning, " + name + ".";
}
}
Interface methods are implicitly public, so implementations must be public.
A default method in an interface can also be overridden by an implementing class, which replaces the interface's provided behavior.
Overriding Versus Overloading
| Overriding | Overloading | |
|---|---|---|
| Where | Subclass replaces an inherited method | Same class (or subclass) adds a method with the same name |
| Parameters | Must be the same | Must differ |
| Return type | Same or covariant | Can differ freely |
| Chosen | At runtime, by the object's actual class | At compile time, by the arguments' declared types |
| Purpose | Specialize behavior for a subtype | Offer the same operation for different inputs |
@Override | Should be used | Not applicable |
Designing for Overriding
Overriding is powerful, but every overridable method becomes an extension point that subclasses may depend on.
- Decide which methods are meant to be overridden, document what an override must do, and make the rest
finalorprivate. - Do not call overridable methods from constructors. The subclass override would run before the subclass's fields are initialized.
- Respect the parent's contract. An override should behave as callers of the parent type expect. A
Squarethat overridessetWidthto also change the height breaks code written forRectangle, even though it compiles. This principle (subtypes must be usable wherever their parent type is expected) is known as the Liskov Substitution Principle.
Practical Example: Notification Templates
import java.util.List;
public class Notifications {
static abstract class Message {
protected final String user;
Message(String user) { this.user = user; }
String subject() { return "Notification"; } // default; may be overridden
abstract String body(); // must be overridden
final String render() { // fixed format; cannot be overridden
return "[" + subject() + "]\nHi " + user + ",\n" + body();
}
@Override
public String toString() { return getClass().getSimpleName() + " for " + user; }
}
static class Welcome extends Message {
Welcome(String user) { super(user); }
@Override String subject() { return "Welcome to GOCOURSE"; }
@Override String body() { return "Your account is ready. Start with Java Basics!"; }
}
static class Reminder extends Message {
private final String course;
Reminder(String user, String course) { super(user); this.course = course; }
@Override String body() { return "You have an unfinished lesson in " + course + "."; }
}
static class UrgentReminder extends Reminder {
UrgentReminder(String user, String course) { super(user, course); }
@Override String subject() { return "Action needed"; }
@Override String body() { return super.body() + " Your access expires in 2 days."; }
}
public static void main(String[] args) {
List<Message> outbox = List.of(
new Welcome("Ada"),
new Reminder("Linus", "Docker"),
new UrgentReminder("Grace", "Django")
);
for (Message m : outbox) {
System.out.println(m);
System.out.println(m.render());
System.out.println();
}
}
}
Output:
Welcome for Ada
[Welcome to GOCOURSE]
Hi Ada,
Your account is ready. Start with Java Basics!
Reminder for Linus
[Notification]
Hi Linus,
You have an unfinished lesson in Docker.
UrgentReminder for Grace
[Action needed]
Hi Grace,
You have an unfinished lesson in Django. Your access expires in 2 days.
render() is final, so every message has the same layout. body() is abstract, so each type must supply it. subject() has a default that Welcome and UrgentReminder override and Reminder keeps. UrgentReminder extends Reminder and overrides body() by calling super.body() and appending a warning, so the reminder text is written once. toString() overrides Object's version and, through getClass(), reports the actual class of each message. The loop treats every item as a Message, and dynamic dispatch selects the right subject and body for each.