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.

Syntax@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 public method cannot become protected in a subclass.
  • The override may not throw new or broader checked exceptions. Unchecked ones are always allowed.
  • final methods cannot be overridden. private methods are not inherited, so a method with the same name in a subclass is unrelated.
  • static methods 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 equals without hashCode, which breaks HashMap and HashSet lookups.

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

Try it yourself
Loading editor…
Press Run to execute the code.

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.

Example · java
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
// true

Covariant 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.

Example · java
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}
// true

Static 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.

Example · java
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

  • Be the first to comment on this lesson.