1. 의미
타입 소거란 컴파일러가 제네릭 타입의 타입 파라미터 정보를 제거하는 것을 의미한다.
즉, 런타임 시점에 제네릭 타입 정보가 제거되는 것이다.
- <T>, <?>, <? super T> → Object
- <T extends Comparable<T>> → Comparable
1) 클래스 타입 소거
// 타입 소거 전 (컴파일 전)
public class Stack<E> {
private E[] stackContent;
public Stack(int capacity) {
this.stackContent = (E[]) new Object[capacity];
}
public void push(E data) {
// ..
}
public E pop() {
// ..
}
}
// 타입 소거 후 (컴파일 후)
public class Stack {
private Object[] stackContent;
public Stack(int capacity) {
this.stackContent = (Object[]) new Object[capacity];
}
public void push(Object data) {
// ..
}
public Object pop() {
// ..
}
}
2) 메서드 타입 소거
// 1) Unbounded Type
// 타입 소거 전 (컴파일 전)
public static <E> void printArray(E[] array) {
for (E element : array) {
System.out.printf("%s ", element);
}
}
// 타입 소거 후 (컴파일 후)
public static void printArray(Object[] array) {
for (Object element : array) {
System.out.printf("%s ", element);
}
}
// 2) Bounded Type
// 타입 소거 전 (컴파일 전)
public static <E extends Comparable<E>> void printArray(E[] array) {
for (E element : array) {
System.out.printf("%s ", element);
}
}
// 타입 소거 후 (컴파일 후)
public static void printArray(Comparable[] array) {
for (Comparable element : array) {
System.out.printf("%s ", element);
}
}
2. 목적
그렇다면 이러한 제네릭 타입 소거는 왜 도입된 것일까? 바로 하위 호환성 때문이다.
제네릭은 JDK 1.5부터 도입되었고, 그 이전까진 아래와 같이 제네릭 타입을 사용하지 않았다.
// 1) 제네릭 도입 전 (JDK 1.5 이전)
ArrayList tvList = new ArrayList();
tvList.add(new TV());
TV t = (TV) tvList.get(0); // Object 타입이 리턴된다.
// 2) 제네릭 도입 후 (JDK 1.5 이후)
ArrayList<TV> tvList = new ArrayList<TV>();
tvList.add(new TV());
TV t = tvList.get(0);
로 타입(raw type)으로 작성된 1번 코드를 최신 JDK에서 실행시키면 정상적으로 동작한다(물론 IDE상에서 경고는 발생할 수 있다). 그러나 아래 제네릭 타입으로 작성된 2번 코드를 JDK1.5에서 실행시키면 동작하지 않는다. JDK 1.5에서는 제네릭 타입을 지원하지 않기 때문이다.
이렇듯 레거시 코드와 제네릭 타입을 함께 사용할 수 있게 해주는 메커니즘이 바로 타입 소거인 것이다.
3. 동작
자바 공식 문서에 따르면 컴파일러가 수행하는 타입 소거의 동작은 다음과 같다.
1) 제네릭 타입에 있는 타입 파라미터를 bounded 유무에 따라 바운디드 타입(bounded type) 또는 Object 타입으로 대체한다
// 타입 소거 전
public class BoundStack<E extends Comparable<E>> {
private E[] stackContent;
// ...
}
// 타입 소거 후
public class BoundStack {
private Comparable [] stackContent;
// ...
}
참고로 대다수의 예제가 위와 같이 상한 경계(upper bounded)만을 예제로 들고 있었다. 하한 경계(lower bounded)는 어떻게 소거되는지 찾아봤는데, Object로 소거된다고 한다. (참고)
2) 만약 타입 안정성을 보존하는 것이 필요하다면 타입 캐스팅 코드를 삽입한다
먼저 간단한 예제를 살펴보자. TV 타입의 List를 선언하고, 각기 다른 타입으로 객체를 꺼내는 상황이다.
// TV 클래스
class TV {
String name;
int price;
public TV(String name, int price) {
this.name = name;
this.price = price;
}
}
List<TV> tvList = new ArrayList<>();
tvList.add(new TV("삼성", 500_000));
tvList.add(new TV("애플", 1000_000));
TV samsungTV = tvList.get(0); // TV 타입으로 받음
Object appleTV = tvList.get(1); // Object 타입으로 받음
위 코드를 디컴파일 해보면 자동적으로 타입 캐스팅 코드가 추가된 것을 확인할 수 있다.
// ...
TV samsungTV = (TV)tvList.get(0); // 컴파일러가 타입 캐스팅 코드를 넣어줌
Object appleTV = tvList.get(1);
3) 제네릭 타입을 확장한 클래스에서 다형성을 보존하기 위해 브릿지 메서드를 생성한다
이 부분은 말이 조금 어렵다. 마찬가지로 코드를 보면 이해가 좀 더 쉬울 것이다.
제네릭 타입 Node<T>와 이를 확장한 MyNode 클래스가 다음과 같이 존재한다.
// 제네릭 타입
public class Node<T> {
public T data;
public Node(T data) { this.data = data; }
public void setData(T data) {
System.out.println("Node.setData");
this.data = data;
}
}
// 제네릭 타입을 상속받은 클래스
public class MyNode extends Node<Integer> {
public MyNode(Integer data) { super(data); }
// Node.setData()를 오버라이딩
public void setData(Integer data) {
System.out.println("MyNode.setData");
super.setData(data);
}
}
타입 소거 후엔 다음과 같이 타입 정보가 제거될 것이다.
이렇게 되면 하위 타입의 메서드 시그니쳐와 상위 타입의 메서드 시그니쳐가 달라지고, 오버라이딩이 일어나지 않게 된다.
public class Node {
public Object data;
public Node(Object data) { this.data = data; }
public void setData(Object data) {
System.out.println("Node.setData");
this.data = data;
}
}
public class MyNode extends Node {
public MyNode(Integer data) { super(data); }
// Node.setData()와 메서드 시그니쳐가 다르므로 메서드 오버라이딩이 일어나지 않는다!
// 이렇게 되면 다형성을 지킬 수 없다!
public void setData(Integer data) {
System.out.println("MyNode.setData");
super.setData(data);
}
}
컴파일러는 이렇게 제네릭 타입의 다형성이 파괴되는 문제를 막고자 상위 타입의 메서드 시그니쳐와 동일한 브릿지 메서드를 추가한다. 이를 통해 메서드 오버라이딩이 이뤄지고, 다형성이 지켜질 수 있다.
class MyNode extends Node {
// ...
// 컴파일러에 의해 생성된 브릿지 메서드
public void setData(Object data) {
setData((Integer) data);
}
public void setData(Integer data) {
System.out.println("MyNode.setData");
super.setData(data);
}
}
4. 참고
'프로그래밍 언어 > Java + Kotlin' 카테고리의 다른 글
[Kotlin] 쿠폰적용가 성능 개선기 (1) | 2023.11.18 |
---|---|
[Kotlin] 특정 프로퍼티 Json 직렬화/역직렬화 시 제외시키기 (0) | 2023.11.09 |
[Java] Collection과 Collections의 차이 (0) | 2023.03.16 |
[Java] 생성자 총정리 (0) | 2023.01.06 |
[Kotlin] 코틀린에서 애너테이션 사용하기 (vs 자바) (0) | 2022.12.19 |