JAVA / INHERITANCE AND POLYMORPHISM
Overriding methods and runtime method selection
Write valid overrides and predict which body runs: the reference type fixes the signature at compile time, the object's class picks the body at run time.
What you will learn
- Match name and parameter types exactly, and let @Override make the compiler check it
- Widen access or narrow the return type in an override, but never the reverse
- Trace any call in two phases: signature from the reference, body from the object
- Spot the overload trap: a changed parameter type creates a new method, not an override
Understanding Overriding methods and runtime method selection
An override replaces an inherited instance method with a new body, and the compiler only treats it as one when the name and the parameter types line up exactly, position by position. Three things are allowed to differ: access may widen (protected to public), the declared return type may be replaced by a subtype, and the throws clause may drop or narrow checked exceptions. Anything else, such as a different parameter type, an extra parameter, or a typo in the name, produces a separate method that quietly coexists with the parent's version. Putting @Override above the method turns that silent mistake into a compile error, which is the whole point of the annotation.
Java resolves a call in two separate passes. javac looks at the compile-time type of the reference, finds the methods with that name whose parameters the arguments fit, and records one signature in the class file; at run time the JVM takes the actual object, starts at its real class, and walks up the chain until it finds a body for that recorded signature. So Cache c = new WarmCache() gives a reference that can only reach methods declared on Cache, yet every such call lands in WarmCache whenever WarmCache supplies a matching body.
The runtime half of that rule applies to the implicit this as well, so a parent method you never touched will dispatch down into the subclass when it calls an overridable method on itself. That is what makes template-style parent methods useful, and it is also why a constructor must not call an overridable method: the subclass body runs before the subclass field initializers do, so it observes 0 and null. Fields take no part in this at all, since a field is chosen by the reference's compile-time type, which means re-declaring one in a subclass hides it instead of overriding it.
public class Main {
public static void main(String[] args) {
Cache plain = new Cache();
Cache warm = new WarmCache(); // reference type Cache, object type WarmCache
System.out.println(plain.get("user:1"));
System.out.println(warm.get("user:1"));
System.out.println(warm.describe()); // describe() is not overridden
}
}
class Cache {
String get(String key) {
return "miss " + key;
}
String describe() {
return getClass().getSimpleName() + " says " + get("user:1");
}
}
class WarmCache extends Cache {
@Override
String get(String key) {
return "hit " + key.toUpperCase();
}
}The reference type decides which method signature a call may use, while the object's actual class decides which method body executes.
Worked examples
An overload that looks like an override
Shows the same object producing two different results because the signature is chosen from the reference type before the object is ever consulted.
public class Main {
public static void main(String[] args) {
LoudFormatter loud = new LoudFormatter();
Formatter viaParent = loud; // same object, wider reference type
System.out.println(loud.format("hi"));
System.out.println(viaParent.format("hi"));
}
}
class Formatter {
String format(Object value) {
return "[" + value + "]";
}
}
class LoudFormatter extends Formatter {
// parameter type differs, so this overloads instead of overriding
String format(String value) {
return value.toUpperCase() + "!";
}
}Example explained
Line 1Formatter declares only format(Object), so that is the single candidate javac can record for viaParent.format("hi").
Line 2LoudFormatter.format(String) has a different parameter type, so it is a new method; adding @Override to it would fail to compile.
Line 3loud.format("hi") sees both methods through a LoudFormatter reference and picks format(String) as the more specific match.
Line 4Both lines call the same object, so the difference in output comes purely from the compile-time signature choice.
Covariant return type and wider access
Demonstrates the two changes an override is permitted to make to the inherited declaration.
public class Main {
public static void main(String[] args) {
Loader generic = new ImageLoader();
System.out.println(generic.load());
ImageLoader precise = new ImageLoader();
Image img = precise.load(); // no cast required
System.out.println(img.width);
}
}
class Loader {
protected Object load() {
return "raw bytes";
}
}
class ImageLoader extends Loader {
@Override
public Image load() {
return new Image(64);
}
}
class Image {
final int width;
Image(int width) {
this.width = width;
}
@Override
public String toString() {
return "Image(" + width + ")";
}
}Example explained
Line 1ImageLoader.load() narrows the return type from Object to Image, which is a legal covariant override.
Line 2It also raises access from protected to public; the reverse direction is rejected with "attempting to assign weaker access privileges".
Line 3generic.load() is typed Object at compile time but runs ImageLoader.load(), and println(Object) then calls the overridden toString().
Line 4precise.load() is typed Image, so img needs no cast, which is the practical payoff of a covariant return.
Important notes
Fields are never overridden. A same-named field in a subclass hides the parent's, and which one you read depends on the compile-time type of the reference, not on the object.
A private method is not inherited, so a subclass method with the same signature is an unrelated new method; calls inside the parent always keep running the parent's private copy.
Common mistakes
Changing the parameter list or misspelling the name and assuming it still overrides: you get an overload, and every call made through a parent-typed reference silently runs the parent body with no compiler complaint.
Trying to make the override less visible, for example a public parent method redeclared as protected or private in the subclass, which fails to compile with "attempting to assign weaker access privileges".
Calling an overridable method from a constructor: the subclass override executes before the subclass field initializers, so it reads 0 or null and often throws NullPointerException in code that looks correct.
Try it yourself
Change, predict, then run
Write a Notifier class with String render(String body) returning body unchanged and void send(String body) printing "sending: " + render(body), then subclass it with UppercaseNotifier that overrides render to uppercase the text. Call send through a variable declared as Notifier and explain why the uppercase version runs even though send was never overridden.
Open the Java workspaceCheck your understanding
Parent declares void run() and void start() { run(); }. Child overrides run(). After Parent p = new Child(); p.start();, why does Child's run() body execute even though start() was compiled against Parent?
- The run() call inside start() is made on the implicit this, and dispatch uses the object's real class, which is Child
- Assigning a Child to a Parent variable permanently replaces Parent's method table with Child's
- Child.run() hides Parent.run(), and hidden methods win when both classes are in the same package
- javac sees Parent p = new Child() and inlines Child.run() into the compiled body of start()
Show answer
Inside start(), run() is an instance call on this; javac records the signature run() from Parent, and the JVM then looks up that signature starting at the object's actual class, finding Child.run(). Option 4 is tempting because javac really does resolve something at compile time, but it resolves only the signature, never the body: the same start() code must work for any subclass, and in general the runtime class is unknown when start() is compiled.