All posts
4 min read

Method overloading vs overriding in Java, explained properly

Both let two methods share a name. One is decided while your code compiles, the other while it runs — and almost every interview question about them is really testing whether you know which.

JavaOOPFundamentals

Every Java interview asks this, and most answers are some version of "overloading is same name different parameters, overriding is same name same parameters." That is true and it explains nothing. It also won't survive the follow-up question.

The real difference is when Java decides which method to call. Overloading is decided while your code is compiling. Overriding is decided while your program is running. Once that clicks, everything else about the two follows from it.

Overloading — the compiler picks

Overloading is one class having several methods with the same name and different parameter lists.

class Invoice {
    double total(double price) {
        return price;
    }

    double total(double price, double taxRate) {
        return price + (price * taxRate);
    }

    double total(double price, double taxRate, double discount) {
        return (price - discount) + ((price - discount) * taxRate);
    }
}

Three methods, one name. When you write invoice.total(500, 0.18), the compiler looks at what you passed — two doubles — and locks in the second method. That decision is baked into the bytecode before your program ever runs.

This is why it's called compile-time polymorphism. The compiler already knows the answer.

It also explains a rule people memorise without understanding:

double total(double price) { ... }
int    total(double price) { ... }   // won't compile

Return type alone can't distinguish them, because the compiler chooses from the call site — and at the call site, total(500) looks identical either way. It has no information to pick with, so it refuses.

Overriding — the object picks

Overriding is a subclass replacing a method it inherited.

class Payment {
    void process() {
        System.out.println("Processing a generic payment");
    }
}

class UpiPayment extends Payment {
    @Override
    void process() {
        System.out.println("Opening UPI app, waiting for PIN");
    }
}

class CardPayment extends Payment {
    @Override
    void process() {
        System.out.println("Charging card, running 3D Secure");
    }
}

Now the interesting part:

Payment payment = new UpiPayment();
payment.process();   // "Opening UPI app, waiting for PIN"

The variable's type is Payment. The compiler only knows it's some kind of Payment — it cannot know which. So it doesn't decide. It defers.

At runtime, the JVM looks at the object actually sitting in memory, sees a UpiPayment, and calls that version. This is runtime polymorphism, and it's the mechanism the whole idea of programming to an interface rests on.

Write one line — payment.process() — and add a NetBankingPayment class six months later without touching it.

Side by side

OverloadingOverriding
DecidedCompile timeRuntime
Decided byThe compiler, from argumentsThe JVM, from the object
WhereOne classParent and child
ParametersMust differMust match exactly
Return typeCan differ (if params do)Same, or a subtype
private / static / finalCan be overloadedCannot be overridden
Access modifierAnythingCannot be more restrictive

The follow-up that catches people

This one comes up constantly, and it's the reason understanding the timing matters more than memorising the table.

class Parent {
    static void greet() { System.out.println("Parent"); }
}

class Child extends Parent {
    static void greet() { System.out.println("Child"); }
}

Parent p = new Child();
p.greet();   // prints "Parent"

Most people say "Child". It prints Parent.

static methods belong to the class, not to any object. There's no object to inspect at runtime, so there's nothing to dispatch on — the JVM never gets involved. The compiler resolves it from the variable's declared type, Parent, and that's final.

This isn't overriding at all. It's hiding, and it behaves completely differently. Add @Override to Child.greet() and the compiler will reject it — which is the single best reason to always write that annotation. It turns a silent misunderstanding into a build error.

Why this actually matters

Overloading is convenience. It saves you from totalWithTax, totalWithDiscount, totalWithTaxAndDiscount cluttering a class.

Overriding is architecture. It's what lets you write code against Payment and extend it later without editing what already works. Every framework you use runs on this — Spring, Android, all of it.

If you only remember one thing: overloading is a question the compiler can answer, overriding is a question only the running program can. Every rule in that table is a consequence of that one sentence.

Takeaways

  • Overloading resolves at compile time from the arguments; overriding resolves at runtime from the object
  • Return type alone can't overload — the call site gives the compiler nothing to work with
  • static methods are hidden, not overridden, and dispatch on the declared type
  • Always write @Override; it converts a silent bug into a compile error

I write about Java, Flutter, and the things I get wrong while building — roughly a couple of times a month, and only when there's something worth saying. Subscribe here if that's useful to you.

ShareXLinkedInWhatsApp

Written by

Zaid Sayyed

Software Engineer · AI & Cross-Platform Apps. Currently open to roles and freelance work.