Method Overriding and super in Java

A subclass replaces inherited behaviour by declaring the same signature, and the JVM chooses the version at runtime from the actual object.

Definition

Overriding is a subclass providing its own implementation of a method already defined in its superclass, using the same name and parameter types. The decision about which version runs is made at runtime, based on the object rather than the declared type.

Example

class Notification {

    void send(String recipient) {
        System.out.println("Generic notification to " + recipient);
    }
}

class EmailNotification extends Notification {

    @Override
    void send(String recipient) {
        System.out.println("Email queued for " + recipient);
    }
}

class SmsNotification extends Notification {

    @Override
    void send(String recipient) {
        System.out.println("SMS queued for " + recipient);
    }
}
List<Notification> channels = List.of(
        new EmailNotification(), new SmsNotification(), new Notification());

for (Notification channel : channels) {
    channel.send("meera@example.com");    // a different method each time
}

The loop variable is a Notification, yet the correct implementation runs for each object. This is dynamic dispatch, and it is the mechanism behind polymorphism.

The rules

ElementRule
Method nameMust be identical
Parameter listMust be identical, in the same order
Return typeSame, or a subtype (covariant return)
AccessSame or wider, never narrower
Checked exceptionsSame, narrower, fewer, or none. Never broader
Unchecked exceptionsNo restriction
static methodsHidden, not overridden
private and final methodsCannot be overridden

Covariant return types

class Document {
    Document copy() { return new Document(); }
}

class Invoice extends Document {
    @Override
    Invoice copy() { return new Invoice(); }    // narrower return, allowed
}

Exception rules in practice

class Reader {
    void load() throws IOException { }
}

class CachedReader extends Reader {
    @Override
    void load() throws FileNotFoundException { }   // narrower, allowed
    // void load() throws Exception { }            // broader, compile error
}

The reason is substitutability: a caller holding a Reader reference has already written a handler for IOException, and must not be surprised by something wider.

The @Override annotation

class Base {
    void process(String value) { }
}

class Derived extends Base {
    @Override
    void proccess(String value) { }   // compile error: nothing is overridden
}
Without @Override, a typo or a changed parameter type silently creates a new method instead of overriding, and the superclass version keeps running. Always write the annotation; it costs nothing and catches a class of bug that is otherwise very hard to see.

The super keyword

class Payment {

    double charge(double amount) {
        return amount;
    }
}

class CardPayment extends Payment {

    @Override
    double charge(double amount) {
        double base = super.charge(amount);    // extend rather than replace
        return base + (base * 0.02);           // add a two percent fee
    }
}

super has two uses: super.method() calls the superclass version, and super(...) calls a superclass constructor as the first statement of a constructor.

Overriding compared with overloading

AspectOverridingOverloading
WhereAcross a class hierarchyWithin one class or inherited
SignatureIdenticalMust differ
BoundRuntime, from the objectCompile time, from declared types
Return typeSame or covariantUnrestricted
PurposeSpecialised behaviourCaller convenience

Overriding Object methods

class Point {

    private final int x;
    private final int y;

    Point(int x, int y) {
        this.x = x;
        this.y = y;
    }

    @Override
    public boolean equals(Object other) {
        if (this == other) {
            return true;
        }
        if (!(other instanceof Point p)) {
            return false;
        }
        return x == p.x && y == p.y;
    }

    @Override
    public int hashCode() {
        return Objects.hash(x, y);
    }

    @Override
    public String toString() {
        return "Point(" + x + ", " + y + ")";
    }
}

Common mistakes

  • Changing a parameter type and accidentally overloading instead of overriding.
  • Narrowing access, which will not compile.
  • Declaring a broader checked exception in the override.
  • Expecting a static method to be overridden; it is hidden and resolved from the declared type.
  • Overriding equals without hashCode, which breaks every hash based collection.
  • Calling an overridable method from a constructor.

Best practices

  • Always annotate with @Override.
  • Preserve the contract of the method you are replacing; do not change what callers can rely on.
  • Call super.method() when the superclass behaviour should still happen.
  • Mark a method final when overriding it would break the class.
  • Keep overrides short; if one grows large, the design probably wants composition.

Practice

  1. Write three subclasses of a Shape class that each override area(), and total the areas in a single loop.
  2. Why does narrowing public to protected in an override fail to compile?
  3. Predict the output when a subclass declares void send(Object recipient) instead of void send(String recipient).
  4. Rewrite CardPayment.charge without super and explain what is lost.
  5. Show what breaks when equals is overridden but hashCode is not, using a HashSet.

Conclusion

Overriding replaces inherited behaviour and is resolved from the real object at runtime. Keep the signature exact, respect the contract, annotate every override, and use super when you mean to extend rather than replace.

Written by Lorens Mishra

Default administrator account created by the installer.

Continue reading

All Java notes →

Discussion

0 comments
Sign in to join the discussion.

No comments yet. Be the first to say something.