Method Overloading / Overriding — Middle¶
What? The detailed rules: overload resolution phases (strict → loose → varargs), most-specific method selection, covariant return types, exception narrowing, generic-induced bridge methods, and the interactions with autoboxing. How? By understanding how the compiler picks an overload at the call site (JLS §15.12) and how the JVM dispatches an override at runtime (JVMS §5.4.5).
1. Three-phase overload resolution¶
JLS §15.12.2 specifies that overload resolution proceeds in three phases:
- Strict invocation — only direct subtype relations, no autoboxing, no varargs.
- Loose invocation — allow autoboxing/unboxing.
- Variable-arity invocation — allow varargs.
The first phase that finds an applicable method wins. Within a phase, the most specific method is chosen.
void m(int x) { ... } // strict
void m(Integer x) { ... } // strict (different params)
void m(int... x) { ... } // varargs
m(5); // phase 1 finds m(int) — wins
void m(Integer x) { ... }
void m(int... x) { ... }
m(5); // phase 1: nothing (m(Integer) needs boxing). phase 2: m(Integer) — wins.
2. Most-specific method¶
When multiple methods are applicable in the same phase, the most specific wins. Specificity rules: - A method m1 is more specific than m2 if every argument that's valid for m1 is also valid for m2.
void m(Object x) { System.out.println("Object"); }
void m(String x) { System.out.println("String"); }
m("hi"); // "String" — String is more specific than Object
If neither is more specific than the other, it's ambiguous → compile error:
void m(Number x, Object y) { ... }
void m(Object x, Number y) { ... }
m(1, 2); // ambiguous — neither more specific
3. Covariant return types¶
Java 5+ allows the override's return type to be a subtype of the parent's:
class Animal { Animal mate() { return null; } }
class Dog extends Animal {
@Override
Dog mate() { return new Dog(); } // covariant: Dog <: Animal
}
The compiler synthesizes a bridge method on Dog:
This preserves binary compatibility — code compiled against Animal.mate() still works.
Primitives don't have covariance: a long override can't return int, even though they're "compatible."
4. Exception narrowing¶
The override can throw fewer or narrower checked exceptions:
class Parent { void m() throws IOException { } }
class Child extends Parent {
@Override void m() throws FileNotFoundException { } // narrower — OK
}
class Bad extends Parent {
@Override void m() throws Exception { } // ERROR — wider
}
Unchecked exceptions (RuntimeException, Error subclasses) are always allowed.
5. Access modifier widening¶
The override can have wider access, but not narrower:
class Parent { protected void m() { } }
class Child extends Parent {
@Override public void m() { } // OK — public is wider
}
class Bad extends Parent {
@Override private void m() { } // ERROR — narrower
}
You can't make a public method private — that would break the LSP (callers expecting public access would fail).
6. Static methods don't override — they hide¶
class Parent {
static String name() { return "Parent"; }
}
class Child extends Parent {
static String name() { return "Child"; } // hides, not overrides
}
Parent p = new Child();
p.name(); // "Parent" — static dispatch via declared type
Child.name(); // "Child"
static methods are dispatched at compile time. The subclass version hides the parent's, but doesn't participate in polymorphism.
@Override doesn't apply to static methods (compile error if used).
7. Final methods can't be overridden¶
class Parent { final void m() { } }
class Child extends Parent {
@Override void m() { } // ERROR — m is final
}
final is sometimes used to enforce template-method patterns: the parent declares final void run() and provides the algorithm, while letting subclasses override smaller protected hooks.
8. Private methods don't override¶
Private methods aren't visible to subclasses, so a same-named private method in a subclass is a separate method:
class Parent {
private String compute() { return "P"; }
public String run() { return compute(); } // calls Parent.compute (private)
}
class Child extends Parent {
private String compute() { return "C"; } // separate method
}
new Child().run(); // "P" — Parent.run sees Parent.compute
Same with protected if accessed from a different package — the protected method is visible only to subclasses, but if the subclass is in the same package, it's accessible to package-mates too.
9. Generics and bridge methods¶
class Box<T> {
T get() { return null; }
}
class IntBox extends Box<Integer> {
@Override Integer get() { return 42; }
}
After erasure, Box.get() has signature Object get(). IntBox.get(): Integer doesn't directly match. The compiler generates a bridge:
IntBox class file:
Integer get(); // user-written
Object get(); // synthetic bridge — calls get():Integer, returns as Object
This is why type erasure is sometimes called the "elephant in the room" — it works, but the mechanism is bytecode-level.
10. Overloading with autoboxing¶
void m(int x) { System.out.println("int"); }
void m(Integer x) { System.out.println("Integer"); }
m(5); // "int" — exact match
m(Integer.valueOf(5)); // "Integer" — exact match
m(null); // "Integer" — null is not assignable to int
Autoboxing happens only if no exact match exists. The compiler always prefers a method that doesn't require boxing/unboxing.
11. Overloading with widening¶
void m(int x) { System.out.println("int"); }
void m(long x) { System.out.println("long"); }
void m(double x) { System.out.println("double"); }
byte b = 5;
m(b); // "int" — byte widens to int
short s = 5;
m(s); // "int" — short widens to int
long l = 5;
m(l); // "long"
The compiler picks the smallest widening conversion that works.
12. Overload resolution gotchas¶
Ambiguous overloads:
void m(Object x, Number y) { }
void m(Number x, Object y) { }
m(1, 2); // ambiguous — both apply, neither more specific
Generic vs raw:
This produces unchecked warnings; avoid raw types.
Varargs ambiguity:
void m(int... a) { }
void m(Integer... a) { }
m(1); // ambiguous in some configurations — boxing rules differ
13. Overriding with generic parameters¶
class Container<T> { void add(T x) { } }
class StringContainer extends Container<String> {
@Override void add(String x) { }
}
After erasure, Container.add has signature add(Object). StringContainer.add(String) doesn't match directly. Bridge method:
StringContainer:
void add(String); // user override
void add(Object); // bridge: cast to String, dispatch to override
The bridge ensures dispatch works regardless of the static type at the call site.
14. Overloading vs overriding cheat sheet¶
| Behavior | Overloading | Overriding |
|---|---|---|
| Resolution time | Compile | Runtime |
| Polymorphic | No | Yes |
| Affects performance | Marginal | Vtable lookup |
| Multiple in same class | Yes | No (one parent) |
| Compiler can mistake for | Override | Overload |
| Annotation | None | @Override |
| Static methods? | Yes (overload) | Hide, not override |
15. What's next¶
| Topic | File |
|---|---|
| JIT view of dispatch | senior.md |
| Bytecode of overload/override | professional.md |
| JLS rules | specification.md |
| Common bugs | find-bug.md |
Memorize this: overloading is compile-time selection of a method by argument types (three-phase resolution). Overriding is runtime dispatch to the receiver's method. Use @Override. Bridge methods bridge between erased generic signatures and covariant returns. Static methods hide; they don't override.