Method Overriding
Replace a superclass method with your own version, follow the rules the compiler enforces, and use @Override so mistakes fail at compile time.
@Override
ReturnType sameName(SameParamTypes p) {
// new behaviour, optionally super.sameName(p)
}A subclass overrides a method by declaring one with the same name and the same parameter types. When the method is called, the JVM looks at the object's real class, not the variable's declared type, and runs the most specific version. That runtime choice is what makes overriding useful.
Always write @Override
@Override asks the compiler to confirm that the method really overrides something. Without it, a typo or a wrong parameter type quietly creates a new method instead. The classic case is equals(User other): it overloads equals(Object) rather than overriding it, so List.contains and HashMap, which call equals(Object), never see it.
The rules
- Same name and parameter types. The return type may be the same or a subtype (a covariant return).
- Access cannot be narrower: a
publicmethod cannot becomeprotectedin a subclass. - The override may not throw new or broader checked exceptions. Unchecked ones are always allowed.
finalmethods cannot be overridden.privatemethods are not inherited, so a method with the same name in a subclass is unrelated.staticmethods are hidden, not overridden: which one runs depends on the declared type, not the object. Fields are hidden the same way.
class Notifier {
void send(String to, String text) { ... }
}
class EmailNotifier extends Notifier {
@Override
void send(String to, String text) { ... }
}Where it matters on the server
You override toString so logs show useful fields and never a password or token. You override equals and hashCode together so entities and DTOs work as map keys and in sets. Framework hooks (a servlet's doGet, a lifecycle callback, a filter method) are overrides too. A typo in the name or a parameter type there means your method is never called and nothing fails; @Override turns that into a compile error.
Mistakes people make
- Overriding a method that the superclass calls internally and then being surprised by the call order (see composition over inheritance).
- Copying the superclass's body into the override instead of calling
super.method()and adding to it. - Overriding
equalswithouthashCode, which breaksHashMapandHashSetlookups.
If a method must not change in subclasses, mark it final. If a whole class is not designed for extension, make the class final.
Example
When to use it
- Override toString on a credentials or token class so it logs a masked value instead of the secret.
- Override equals and hashCode on an entity so it can be found in a HashSet of already-processed records.
- Override a send method per channel so a notification service can treat email, SMS and webhooks the same way.
More examples
equals that overloads instead of overriding
List.contains calls equals(Object), so it falls back to Object's identity check for WrongUser. The correct override, with hashCode alongside it, also makes User safe as a HashMap key.
class WrongUser {
final String email;
WrongUser(String email) { this.email = email; }
// Overload: takes WrongUser, not Object. @Override here would not compile.
boolean equals(WrongUser other) { return email.equals(other.email); }
}
class User {
final String email;
User(String email) { this.email = email; }
@Override
public boolean equals(Object o) {
return o instanceof User other && email.equals(other.email);
}
@Override
public int hashCode() { return email.hashCode(); }
}
void main() {
List<WrongUser> wrong = List.of(new WrongUser("[email protected]"));
List<User> right = List.of(new User("[email protected]"));
IO.println(wrong.contains(new WrongUser("[email protected]")));
IO.println(right.contains(new User("[email protected]")));
}
// Output:
// false
// trueCovariant return types
Because JsonRequest.copy() may declare JsonRequest as its return type, callers holding a JsonRequest keep the specific type without casting. Callers holding a Request still get a valid Request.
class Request {
final String path;
Request(String path) { this.path = path; }
Request copy() { return new Request(path); }
}
class JsonRequest extends Request {
final String body;
JsonRequest(String path, String body) {
super(path);
this.body = body;
}
// Overrides copy() but returns the narrower type.
@Override
JsonRequest copy() { return new JsonRequest(path, body); }
}
void main() {
JsonRequest original = new JsonRequest("/orders", "{\"qty\":2}");
JsonRequest copy = original.copy(); // no cast needed
IO.println(copy.path + " " + copy.body);
IO.println(copy != original);
}
// Output:
// /orders {"qty":2}
// trueStatic methods are hidden, not overridden
Calling a static method through an instance compiles, but it binds to the variable's declared type, which surprises people. Call static methods through the class name so there is nothing to misread.
class Parent {
static String kind() { return "parent static"; }
String name() { return "parent instance"; }
}
class Child extends Parent {
static String kind() { return "child static"; }
@Override String name() { return "child instance"; }
}
void main() {
Parent p = new Child();
// Instance method: chosen by the object's class at runtime.
IO.println(p.name());
// Static method: chosen by the declared type at compile time.
IO.println(p.kind());
IO.println(Child.kind());
}
// Output:
// child instance
// parent static
// child static
Discussion