Method overloading vs overriding
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
| Overloading | Overriding | |
|---|---|---|
| Where | Same class (or inherited methods) | Subclass redefines a parent method |
| Method name | Same | Same |
| Parameters | Must differ (number, types or order) | Must be identical |
| Return type | Can be anything | Same type or a subtype (covariant) |
| Access modifier | Any | Same or wider (never narrower) |
| Checked exceptions | Any | Same, narrower or none (never broader) |
| static / private / final methods | Can be overloaded | Can't be overridden (static ones are hidden) |
| Chosen | At compile time, by declared argument types | At run time, by the object's class |
| Annotation | None | @Override (always add it) |
Example
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 timestatic 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.