Skip to content
Java polymorphism 6 min read

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

FeatureOverloadingOverriding
Where it happensSame classSubclass
Resolved byCompiler (compile-time)JVM (runtime)
What changesParameter listMethod body
Also calledStatic polymorphismDynamic 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 @Override when you intend to override a method. The compiler will then catch typos or signature mismatches for you.

Side-by-Side Comparison

AspectOverloadingOverriding
ClassSame classParent & subclass
Method signatureMust differ (parameter list)Must be identical
Return typeCan differ freelyMust be same or covariant
static methodsCan be overloadedCannot be overridden (only hidden)
private methodsCan be overloadedCannot be overridden
final methodsCan be overloadedCannot be overridden
Access modifierNo restrictionCannot be more restrictive
ExceptionNo restrictionCannot throw broader checked exceptions
Polymorphism typeCompile-timeRuntime
BindingEarly (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. With static methods the compiler locks in Parent at 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) and add(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()).
Last updated June 13, 2026
Was this helpful?