🧬 Inheritance & Polymorphism · Intermediate

Overloading vs overriding in Java

Overloads are chosen at compile time by declared types; overrides at runtime.

🧩 The mysteryA variable holds a String. You pass it to a method that has a String overload... and Java calls the Object version instead. A bug? No — a matter of timing.

Overloading: same name, new parameters

Overloaded methods share a name but differ in their parameter lists (number, types or order). The compiler picks one, using the declared types of the arguments.

void p(Object o) { ... }
void p(String s) { ... }
🔮 Predict it

Who gets the call?

What does this print?

class Greeter {
    void hi(Object o) {
        System.out.print("obj "); }
    void hi(String s) {
        System.out.print("str "); }
}
void main() {
    var g = new Greeter(); Object x = "Ann";
    g.hi(x); g.hi("Ann");
}
  1. str str
  2. obj str
  3. obj obj
Show the answer

obj str. x holds a String, but it's declared Object, and overloads are chosen at compile time from declared types. So hi(Object) wins for x.

Compile time vs runtime

Overloading = choosing among *signatures*, at compile time, by declared types. Overriding = choosing among *implementations* of one signature, at runtime, by the object's class. To overload, change the parameters; to override, keep the exact signature.

🤔 Think first

Both at once

A has f(Object). B extends A overrides f(Object) and adds f(String). With A a = new B();, what does a.f("x") run?

Think about it, then reveal the answer

**B's f(Object).** Step 1, compile time: A only offers f(Object), so that signature is locked in. Step 2, runtime: the object is a B, so B's override of f(Object) runs. B's f(String) was never even considered.

⚠️ The trap

Return type isn't enough

Two methods with the same name and parameters but different return types are not overloads — the compiler reports a duplicate method. A call like size() would give it no way to choose.

int size() { return 1; }
long size() { return 2L; } // error!
💼 In the real world

In real projects

A famous overload trap: on a List<Integer>, list.remove(1) calls remove(int index) and removes the element at position 1 — not the value 1. To remove the value, write list.remove(Integer.valueOf(1)).

Key takeaways

  1. Overload: different parameters, chosen at compile time
  2. Override: same signature, chosen at runtime
  3. Overload choice uses declared argument types
  4. Methods can't differ only by return type
🤯 Did you know?

System.out.println has 10 overloads: no arguments, boolean, char, int, long, float, double, char[], String and Object.

Practice questions

What does this print?

class Printer {
    void p(Object o) { System.out.print("Object "); }
    void p(String s) { System.out.print("String "); }
}
void main() {
    var pr = new Printer();
    Object o = "hi";
    pr.p(o);
    pr.p("hi");
}
  1. String String
  2. Object String
  3. Object Object
  4. String Object
Check your answer

Object String. Overloads are picked at compile time from declared types. o is declared Object, so p(Object) is chosen even though it holds a String.

What does this print?

class A {
    void f(Object o) { System.out.print("A.obj"); }
}
class B extends A {
    void f(Object o) { System.out.print("B.obj"); }
    void f(String s) { System.out.print("B.str"); }
}
void main() {
    A a = new B(); a.f("x");
}
  1. B.str
  2. B.obj
  3. A.obj
Check your answer

B.obj. At compile time A only offers f(Object), so that signature is chosen. At runtime it dispatches to B's override of f(Object). B's f(String) is never considered.

Next: treating a Dog as an Animal is easy. Going back is where ClassCastException lurks.