Stage 2: Object-oriented programming, lesson 6 of 15

Method overloading vs overriding

Beginner3 min read@since 8Code runs on your Java 25
Explain it forThe essentials plus production detail and pitfalls.

Two features share a name but work at different times:

  • Overloading: several methods with the same name but different parameter lists in one class. The compiler picks one at compile time, from the declared types of the arguments. This is compile-time polymorphism.
  • Overriding: a subclass gives its own version of an inherited method with the same signature. The JVM picks the version at run time, from the object's actual class. This is runtime polymorphism.

Overloading is about convenience (one name, many ways to call it). Overriding is about behaviour (each subclass does the job its own way), and it's what makes polymorphism work.

Side by side

OverloadingOverriding
WhereSame class (or inherited methods)Subclass redefines a parent method
Method nameSameSame
ParametersMust differ (number, types or order)Must be identical
Return typeCan be anythingSame type or a subtype (covariant)
Access modifierAnySame or wider (never narrower)
Checked exceptionsAnySame, narrower or none (never broader)
static / private / final methodsCan be overloadedCan't be overridden (static ones are hidden)
ChosenAt compile time, by declared argument typesAt run time, by the object's class
AnnotationNone@Override (always add it)

Example

Java
class Notifier {
    void send(String message) { System.out.println("Sending: " + message); }
    void send(String message, int retries) { System.out.println("Sending " + message + " with " + retries + " retries"); }  // overload
    void send(List<String> messages) { messages.forEach(this::send); }                                                   // overload
}

class SmsNotifier extends Notifier {
    @Override
    void send(String message) {                        // override: same signature, new behaviour
        System.out.println("SMS: " + message.substring(0, Math.min(160, message.length())));
    }
}

Notifier n = new SmsNotifier();
n.send("Your course is ready");        // SMS: Your course is ready  (override chosen at run time)
n.send("Your course is ready", 3);     // overload chosen at compile time
The classic trap: overloads are chosen by the declared type
static void greet(Object o) { System.out.println("object"); }
static void greet(String s) { System.out.println("string"); }

Object o = "hello";
greet(o);            // prints "object": the declared type is Object
greet("hello");      // prints "string"

Common mistake

Writing equals(MyType other) instead of equals(Object other). It compiles, but it's an overload, so collections ignore it.

Under the hood

Overload resolution happens in phases: exact match first, then widening (int to long), then boxing (int to Integer), then varargs. Mixing overloads with boxing and varargs makes calls hard to read, so keep overloads clearly different. The most famous overriding bug is public boolean equals(Point p): it overloads equals(Object) instead of overriding it, so HashSet and HashMap never call it. @Override turns that mistake into a compile error.

Check yourself

Object o = "hi"; with greet(Object) and greet(String) defined, what does greet(o) call?

How this connects

Part of OOP in practice.

Was this lesson helpful?

Finished reading? Mark it complete to track your progress.