Overloading vs Overriding
Method overloading and method overriding are two of Java’s most fundamental — and most confused — features. Both let you use the same method name for different behavior, but they work at completely different times and for completely different reasons.
The One-Line Distinction
| Feature | Overloading | Overriding |
|---|---|---|
| Where it happens | Same class | Subclass |
| Resolved by | Compiler (compile-time) | JVM (runtime) |
| What changes | Parameter list | Method body |
| Also called | Static polymorphism | Dynamic polymorphism |
If you remember nothing else: overloading is about same name, different parameters in the same class; overriding is about same signature, different body in a subclass.
Method Overloading
Method overloading means defining multiple methods with the same name inside a single class, differentiated by their parameter list (number, type, or order of parameters).
class Printer {
void print(String text) {
System.out.println("String: " + text);
}
void print(int number) {
System.out.println("Int: " + number);
}
void print(String text, int copies) {
for (int i = 0; i < copies; i++) {
System.out.println(text);
}
}
}
public class OverloadDemo {
public static void main(String[] args) {
Printer p = new Printer();
p.print("Hello"); // calls print(String)
p.print(42); // calls print(int)
p.print("Hi", 2); // calls print(String, int)
}
}
Output:
String: Hello
Int: 42
Hi
Hi
The compiler picks the right version at compile time by matching the argument types. This is compile-time polymorphism.
Note: Changing only the return type does NOT constitute overloading. The compiler uses parameter lists to distinguish methods — not return types.
Method Overriding
Method overriding means a subclass provides its own implementation of a method that is already defined in its superclass, using the exact same signature (name + parameters + return type).
class Animal {
void speak() {
System.out.println("Animal makes a sound");
}
}
class Dog extends Animal {
@Override
void speak() {
System.out.println("Dog barks");
}
}
class Cat extends Animal {
@Override
void speak() {
System.out.println("Cat meows");
}
}
public class OverrideDemo {
public static void main(String[] args) {
Animal a1 = new Dog(); // runtime type is Dog
Animal a2 = new Cat(); // runtime type is Cat
a1.speak(); // Dog barks
a2.speak(); // Cat meows
}
}
Output:
Dog barks
Cat meows
Even though both variables are declared as Animal, the JVM calls the method on the actual object at runtime. This is runtime polymorphism.
Tip: Always use
@Overridewhen you intend to override a method. The compiler will then catch typos or signature mismatches for you.
Side-by-Side Comparison
| Aspect | Overloading | Overriding |
|---|---|---|
| Class | Same class | Parent & subclass |
| Method signature | Must differ (parameter list) | Must be identical |
| Return type | Can differ freely | Must be same or covariant |
static methods | Can be overloaded | Cannot be overridden (only hidden) |
private methods | Can be overloaded | Cannot be overridden |
final methods | Can be overloaded | Cannot be overridden |
| Access modifier | No restriction | Cannot be more restrictive |
| Exception | No restriction | Cannot throw broader checked exceptions |
| Polymorphism type | Compile-time | Runtime |
| Binding | Early (static) | Late (dynamic) |
Access Modifier Rule for Overriding
When you override a method, the overriding method’s access level must be the same or wider than the original.
class Base {
protected void show() {
System.out.println("Base show");
}
}
class Child extends Base {
@Override
public void show() { // widening protected → public: OK
System.out.println("Child show");
}
}
The reverse — narrowing public to protected — causes a compile error.
Exception Rule for Overriding
The overriding method cannot declare new or broader checked exceptions than the superclass method.
class Base {
void readFile() throws IOException { }
}
class Child extends Base {
@Override
void readFile() throws FileNotFoundException { } // OK — narrower checked exception
// void readFile() throws Exception { } // COMPILE ERROR — broader
}
Unchecked exceptions (RuntimeException and subclasses) have no such restriction.
Static Methods: Hiding, Not Overriding
static methods belong to the class, not an instance. If a subclass declares a static method with the same signature, it hides the parent method rather than overriding it. The version that runs depends on the reference type, not the actual object.
class Parent {
static void greet() {
System.out.println("Hello from Parent");
}
}
class Child extends Parent {
static void greet() {
System.out.println("Hello from Child");
}
}
public class HidingDemo {
public static void main(String[] args) {
Parent obj = new Child();
obj.greet(); // resolved at compile time using reference type
}
}
Output:
Hello from Parent
Warning: This behavior surprises many developers. With instance methods the JVM dispatches to
Child. Withstaticmethods the compiler locks inParentat compile time. This is called method hiding, not overriding.
Covariant Return Types
Since Java 5, an overriding method may return a subtype of the original return type. This is called a covariant return type.
class Shape {
Shape create() {
return new Shape();
}
}
class Circle extends Shape {
@Override
Circle create() { // return type narrows to Circle — valid
return new Circle();
}
}
This does not apply to overloading; with overloading you can choose any return type independently.
Under the Hood
Overloading: compile-time resolution
When the compiler sees p.print("Hello"), it inspects all print methods in Printer, scores each candidate against the argument types, and emits a direct invokevirtual (or invokestatic) call to the winning method’s bytecode descriptor. No decision is deferred to runtime. You can confirm this with javap -c OverloadDemo.class — the constant pool entry already names the exact descriptor.
Overriding: vtable dispatch
Overriding relies on the JVM’s vtable (virtual method table). Every class has a vtable — an array of method pointers. When Dog overrides speak, the JVM replaces the Animal::speak slot in Dog’s vtable with a pointer to Dog::speak. An invokevirtual instruction at runtime dereferences the actual object’s vtable, so a1.speak() finds Dog::speak even though the variable is typed as Animal.
See vtable & Dynamic Dispatch for a full walkthrough of how the JVM performs this lookup.
Performance impact
Overloaded calls resolve at compile time — essentially zero overhead. Overridden calls go through vtable lookup, which is a tiny indirection, but the JIT compiler typically inlines monomorphic call sites after a short warmup, making the runtime cost negligible for hot code. See JIT Compilation & Bytecode for how this inlining works.
Quick Decision Guide
Use overloading when:
- You want the same operation to accept different input types (e.g.,
add(int, int)andadd(double, double)). - Convenience constructors — provide multiple ways to construct the same object.
Use overriding when:
- A subclass needs to specialize inherited behavior (e.g.,
Dog.speak()vs.Animal.speak()). - You are implementing a framework contract (e.g., overriding
toString(),equals(),hashCode()).
Related Topics
- Method Overloading — a deep dive into how the compiler resolves overloaded calls
- Method Overriding — full rules, examples, and edge cases for overriding
- Compile-Time Polymorphism — overloading as the mechanism for static dispatch
- Runtime Polymorphism — overriding as the mechanism for dynamic dispatch
- Covariant Return Type — how return types can narrow in overriding methods
- vtable & Dynamic Dispatch — JVM internals behind method overriding