Java에서 제네릭과 메서드 오버로딩이 함께 사용될 때, 우리가 기대했던 것과는 다른 결과가 나오는 경우가 있습니다.
이번 글에서는 아래 코드에서 왜 B0가 출력되는지 심층적으로 분석해 보겠습니다.
코드 분석
class Main {
public static class Collection<T> {
T value;
public Collection(T t) {
value = t;
}
public void print() {
new Printer().print(value);
}
class Printer {
void print(Integer a) {
System.out.print("A" + a);
}
void print(Object a) {
System.out.print("B" + a);
}
void print(Number a) {
System.out.print("C" + a);
}
}
}
public static void main(String[] args) {
new Collection<>(0).print();
}
}
위 코드를 실행하면 B0가 출력됩니다.
하지만 우리는 print(Integer a) 또는 print(Number a)가 호출될 것이라고 예상할 수도 있습니다.
그 이유를 하나씩 살펴보겠습니다.
1. 제네릭 타입 결정
new Collection<>(0);
위 코드에서 Collection <T> Collection <T>의 타입 매개변수 T는 Integer로 추론됩니다.
따라서 Collection <Integer> Collection <Integer>가 됩니다.
즉, value의 타입은 Integer가 됩니다.
그런데 print() 메서드를 호출하면 다음 메소드가 작동하게 됩니다.
new Printer().print(value);
이때, value는 Integer 타입이므로 print(Integer a), print(Object a), print(Number a) 중 하나가 호출될 것입니다.
2. 오버로딩과 정적 바인딩
Java의 메서드 오버로딩은 컴파일 타임에 정적으로 결정됩니다.
즉, 컴파일러는 print(value)가 호출될 때, value의 컴파일 시점 타입을 기준으로 가장 적절한 메서드를 선택합니다.
하지만 중요한 점은, 제네릭에서 T는 컴파일 시점에는 그냥 타입 매개변수일뿐이며, 구체적인 타입으로 변환되지 않는다는 것입니다.
즉, T는 Integer이지만, 컴파일러는 Type을 Object로 간주하여 처리합니다.
결국, print(value);는 아래와 동일하게 해석됩니다.
new Printer().print((Object) value);
따라서 가장 적합한 메서드는 print(Object a)가 되고, 결과적으로 "B0"가 출력됩니다.
3. 왜 print(Integer a)나 print(Number a)가 호출되지 않을까?
제네릭 코드에서 타입이 특정 타입으로 지정되었더라도, 컴파일 타임에는 T 자체가 Object로 간주됩니다.
즉, T를 포함한 연산에서는 T가 Object 타입으로 처리되는 경우가 많습니다.
아래 코드로 비교해 보겠습니다.
일반적인 경우:
class Test {
void print(Integer a) { System.out.print("A" + a); }
void print(Object a) { System.out.print("B" + a); }
void print(Number a) { System.out.print("C" + a); }
public static void main(String[] args) {
Test test = new Test();
test.print(0); // "A0" 출력
}
}
위 코드에서는 0이 Integer 타입이므로 print(Integer a)가 정확히 호출됩니다.
제네릭을 사용한 경우:
class Collection<T> {
T value;
public Collection(T t) { value = t; }
void print() { new Printer().print(value); }
class Printer {
void print(Integer a) { System.out.print("A" + a); }
void print(Object a) { System.out.print("B" + a); }
void print(Number a) { System.out.print("C" + a); }
}
}
public class Main {
public static void main(String[] args) {
new Collection<>(0).print(); // "B0" 출력
}
}
여기서 T는 Integer이지만, 컴파일 타임에는 Object로 간주되므로 print(Object a)가 호출됩니다.
4. 해결 방법
만약 print(Integer a)가 호출되길 원한다면, 다음과 같이 해결할 수 있습니다.
명시적 캐스팅 사용
void print() {
new Printer().print((Integer) value);
}
위처럼 명시적으로 Integer로 캐스팅하면, print(Integer a)가 호출됩니다.
타입 상한 경계 설정
class Collection<T extends Integer> {
T value;
public Collection(T t) { value = t; }
void print() { new Printer().print(value); }
class Printer {
void print(Integer a) { System.out.print("A" + a); }
void print(Object a) { System.out.print("B" + a); }
void print(Number a) { System.out.print("C" + a); }
}
}
public class Main {
public static void main(String[] args) {
new Collection<>(0).print(); // "A0" 출력
}
}
이처럼 T extends Integer를 사용하면 T는 최소한 Integer로 간주되므로 print(Integer a)가 호출됩니다.
정리
- 제네릭의 타입은 컴파일 타임에 Object로 간주된다.
- 오버로딩된 메서드는 컴파일 시점에 정적으로 결정된다.
- 명시적 캐스팅 또는 타입 상한 경계 설정을 사용하면 원하는 메서드를 호출할 수 있다.
정처기 실기를 준비 중에 만난 문제에서 그동안 놓쳤던 부분을 알게 되어 작성하게 되었습니다.
당연히 Integer를 호출할 것이라 생각하고, Type을 찍어도 Integer를 출력하지만 답은 "B0"가 나오는 상황이 답답하여 정리하게 되었는데요.
이처럼 놓친 내용들이 있는지 확인하며 기본기를 탄탄히 다 저나 갈 생각입니다.
'Java' 카테고리의 다른 글
[ThreadLocal] Thread영역에 데이터를 저장하고 싶어요 (1) | 2025.01.27 |
---|---|
오차 없는 실수 연산 어떻게 해야 할까? (0) | 2025.01.10 |
[Java] Static (3) | 2024.12.26 |
[ Basic ] I/O Stream이란? (0) | 2024.12.23 |
[JPA] 비관적 락 낙관적 락 (0) | 2024.06.29 |