What's the Difference Between Overloading and Overriding?
Overloading and overriding both deal with methods that share the same name. However, one involves differences in input parameters, while the other involves redefining behavior. We'll examine this distinction through code examples.
The reason these two concepts are confusing is simple: they share the same method name.
However, the reasons they use the same name, how they work, and their purposes are all different.
Overloading: Same Name, Different Input
Overloading is creating multiple methods with the same name but differing in the number, type, or order of parameters. It's primarily seen within the same class, but can also appear in inheritance relationships where a child class adds a method with the same name as a parent method but different parameters.
According to the Java Language Specification, a method signature is defined by the method name and formal parameter types. The return type is not included in the signature. Therefore, overloading does not work if only the return type differs.
class Calculator {
int add(int a, int b) {
return a + b;
}
double add(double a, double b) {
return a + b;
}
int add(int a, int b, int c) {
return a + b + c;
}
}
Calculator calc = new Calculator();
System.out.println(calc.add(1, 2)); // 3
System.out.println(calc.add(1.5, 2.5)); // 4.0
System.out.println(calc.add(1, 2, 3)); // 6
Although the method name add is the same, Java determines which method to call by looking at the differences in parameters. This decision is made at compile time.
Overloading does not work if only the return type differs.
class Example {
int getValue() { return 1; }
double getValue() { return 1.0; } // Compilation error
}
If you just write getValue(), the compiler cannot determine which method to use. This is because the return type is not included in the method signature.
Real-world Example: Order Amount Calculation
class OrderCalculator {
int calculate(int quantity) {
return quantity * 10000;
}
int calculate(int quantity, int couponAmount) {
return quantity * 10000 - couponAmount;
}
int calculate(int quantity, double discountRate) {
return (int)(quantity * 10000 * (1 - discountRate));
}
}
OrderCalculator calc = new OrderCalculator();
System.out.println(calc.calculate(3)); // 30000
System.out.println(calc.calculate(3, 5000)); // 25000
System.out.println(calc.calculate(3, 0.1)); // 27000
The purpose of 'calculating order amount' is the same, but the input conditions differ. Overloading is a way to group methods under the same name when the purpose is the same but the inputs vary.
Pitfall Example: Different Variable Types Call Different Methods
class MessagePrinter {
void print(String message) {
System.out.println("String: " + message);
}
void print(Object message) {
System.out.println("Object: " + message);
}
}
MessagePrinter printer = new MessagePrinter();
String text = "hello";
Object objectText = "hello";
printer.print(text); // String: hello
printer.print(objectText); // Object: hello
Although the actual values are both strings, the results differ. Overloading selects the method based on the variable's type at compile time, not the actual runtime value. The declared type, not the runtime value, is what matters.
Overriding: Redefining Inherited Behavior
Overriding is when a child class redefines a method from the parent class with the same name and same parameters in an inheritance relationship.
class Animal {
void sound() {
System.out.println("...");
}
}
class Dog extends Animal {
@Override
void sound() {
System.out.println("멍멍");
}
}
class Cat extends Animal {
@Override
void sound() {
System.out.println("야옹");
}
}
Dog dog = new Dog();
Cat cat = new Cat();
dog.sound(); // 멍멍
cat.sound(); // 야옹
@Override is an annotation that tells the compiler that you are redefining a parent method. While it can be omitted, including it catches typos in method names with a compile error.
Real-world Example: Payment Processing by Payment Method
class Payment {
void pay(int amount) {
System.out.println(amount + "원을 결제합니다.");
}
}
class CardPayment extends Payment {
@Override
void pay(int amount) {
System.out.println("카드로 " + amount + "원을 결제합니다.");
}
}
class KakaoPayPayment extends Payment {
@Override
void pay(int amount) {
System.out.println("카카오페이로 " + amount + "원을 결제합니다.");
}
}
Payment payment = new CardPayment();
payment.pay(10000); // 카드로 10000원을 결제합니다.
payment = new KakaoPayPayment();
payment.pay(15000); // 카카오페이로 15000원을 결제합니다.
Although the variable type is Payment, the method that executes differs based on the actual object. Overriding operates at runtime based on the actual object. This is dynamic binding, and it is the core of polymorphism.
Example Using super
This is when a child class doesn't completely discard the parent's behavior but adds additional behavior on top of it.
class Notification {
void send(String message) {
System.out.println("알림 전송: " + message);
}
}
class EmailNotification extends Notification {
@Override
void send(String message) {
super.send(message);
System.out.println("이메일 로그를 저장합니다.");
}
}
EmailNotification email = new EmailNotification();
email.send("주문이 완료되었습니다.");
// 알림 전송: 주문이 완료되었습니다.
// 이메일 로그를 저장합니다.
super.send(message) executes the parent method first and layers the child's behavior on top of it.
static Methods Are Not Overriding
class Parent {
static void hello() {
System.out.println("Parent");
}
}
class Child extends Parent {
static void hello() {
System.out.println("Child");
}
}
Parent p = new Child();
p.hello(); // Parent
Parent.hello(); // Parent
Child.hello(); // Child
p.hello() looks like overriding, but it is actually interpreted based on p's compile-time type, Parent. Since static methods do not apply dynamic binding, the parent's method executes even if the actual runtime object is Child. Declaring the same name again is not overriding but method hiding.
In practice, to avoid this confusion, calling with the class name like Parent.hello() is recommended. Overriding only works with instance methods.
At a Glance Comparison
| Overloading | Overriding | |
|---|---|---|
| Location | Mainly within the same class, also possible in inheritance relationships | Inheritance relationship |
| Method Name | Same | Same |
| Parameters | Must differ | Must be the same |
| Return Type | Cannot be distinguished by return type alone | Same or more specific type possible |
| Decision Point | Compile time | Runtime |
The reason overriding's return type is "same or more specific" rather than "must be the same" is that Java allows covariant return types. It is possible to override a parent method with a more specific subtype than the parent method's return type.
Practice Problems
Problem 1
Choose all correct overloading declarations from the following:
class Test {
void print(int a) {} // (1)
void print(int a, int b) {} // (2)
void print(double a) {} // (3)
}
And if the following declaration is added to the same Test class, does overloading hold?
int print(int a) {} // (4)
① (1), (2), (3) all hold, and (4) also holds
② (1), (2), (3) hold, and (4) is a compile error
③ (3) does not hold
④ Overloading always holds when return types differ
② (1), (2), (3) hold, and (4) is a compile error
(1), (2), (3) hold overloading because the parameter types or number of parameters differ. (4) differs only in return type while the parameters are identical to (1). Since return type is not included in the method signature, overloading does not occur and a compile error is generated.
</details>Problem 2
What is the output when the following code is executed?
class Parent {
void show() {
System.out.println("Parent");
}
}
class Child extends Parent {
@Override
void show() {
System.out.println("Child");
}
}
public class Main {
public static void main(String[] args) {
Parent p = new Child();
p.show();
}
}
① Parent
② Child
③ Parent
Child
④ Compile error
② Child
The variable type is Parent, but the actual object is Child. Overridden methods are invoked based on the actual object at runtime (dynamic binding). Therefore, Child's show() is executed.
Problem 3
What is the output when the following code is executed?
class Printer {
void print(String value) {
System.out.println("String: " + value);
}
void print(Object value) {
System.out.println("Object: " + value);
}
}
public class Main {
public static void main(String[] args) {
Printer printer = new Printer();
String text = "hello";
Object obj = "hello";
printer.print(text);
printer.print(obj);
}
}
① String: hello
String: hello
② String: hello
Object: hello
③ Object: hello
Object: hello
④ Compile error
② String: hello / Object: hello
Overloading selects a method based on the variable type at compile time. text is declared as String type, so print(String value) is called, and obj is declared as Object type, so print(Object value) is called. Even if the actual values at runtime are both strings, the determination criterion is the declared type.
When distinguishing between overloading and overriding, starting from the fact that names are the same leads to continued confusion. It is more accurate to first check whether the input differs or whether inherited behavior is redefined. Overloading is about dividing inputs, and overriding is about redefining behavior.