제네릭 공식문서 파해치기 (8)
안녕하세요 🐸 !
드디어 제네릭의 마지막 파트 타입 소거 입니다.
제네릭 & 제네릭 메서드의 타입 소거
자바 컴파일러는 타입 변수를 지우고 대체합니다.
타입 변수에 경계를 명시했다면 해당 첫 번째로 명시한 타입으로 대체됩니다.
예로 가장 기본적인 제네릭 사용법인 <T>
는 Object
로 컴파일됩니다.
기본 예제
예제 - 작성 코드
1
2
3
4
5
6
7
8
9
10
11
12
13
public class Node<T> {
private T data;
private Node<T> next;
public Node(T data, Node<T> next) {
this.data = data;
this.next = next;
}
public T getData() { return data; }
// ...
}
예제 - 컴파일 코드
1
2
3
4
5
6
7
8
9
10
11
12
13
public class Node {
private Object data;
private Node next;
public Node(Object data, Node next) {
this.data = data;
this.next = next;
}
public Object getData() { return data; }
// ...
}
extends
혹은 super
를 통해 타입에 제한을 뒀을 경우 그 첫 번째 타입으로 변환됩니다.
예제 - 작성 코드
1
2
3
4
5
6
7
8
9
10
11
12
13
public class Node<T extends Comparable<T>> {
private T data;
private Node<T> next;
public Node(T data, Node<T> next) {
this.data = data;
this.next = next;
}
public T getData() { return data; }
// ...
}
예제 - 컴파일 코드
1
2
3
4
5
6
7
8
9
10
11
12
13
public class Node {
private Comparable data;
private Node next;
public Node(Comparable data, Node next) {
this.data = data;
this.next = next;
}
public Comparable getData() { return data; }
// ...
}
브릿지 메서드
컴파일러가 제네릭의 타입을 소거하면서 발생하는 문제로 상속 받은 메서드의 타입이 서로 일치하지 않게 되어버리는 문제가 있습니다. 이 문제를 해결하기 위해 컴파일러는 자동으로 브리지 메서드를 생성합니다.
먼저 공식 문서의 이해를 위한 코드를 보겠습니다.
우리가 작성한 코드가 아래와 같다고 가정해보겠습니다.
타입 소거 전 예시)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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); }
public void setData(Integer data) {
System.out.println("MyNode.setData");
super.setData(data);
}
}
위에서 MyNode
는 Node
를 상속받아 setData()
메서드를 구현하고 있습니다.
이제 컴파일러가 타입 소거한 이후의 코드를 확인해보겠습니다.
타입 소거 후 예시)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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); }
public void setData(Integer data) {
System.out.println("MyNode.setData");
super.setData(data);
}
}
이제 코드를 보면 의아한 점이 생깁니다.
분명 Node
를 상속받아 setDate()
를 구현했음에도 불구하고 매개 변수의 타입이 서로 다르기 때문에 컴파일된 코드는 오버라이딩이 아닌 오버로딩을 하고 있습니다.
이 논리 오류를 해결하기 위해 컴파일러는 브릿지 메서드를 생성합니다.
공식 문서의 브릿지 메서드 생성 예시입니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class MyNode extends Node {
// Bridge method generated by the compiler
//
public void setData(Object data) {
setData((Integer) data);
}
public void setData(Integer data) {
System.out.println("MyNode.setData");
super.setData(data);
}
// ...
}
위에서 작성한 타입 소거 전 예시에서는 T
가 Integer
가 되었기 때문에 브릿지 메서드 (setData((Integer) data);
) 를 생성합니다.
앞서 작성한 위의 환경에서 아래의 코드를 실행 시키면 에러가 발생하게 됩니다.
1
2
3
4
5
6
7
8
MyNode mn = new MyNode(5);
Node n = mn; // A raw type - compiler throws an unchecked warning
// Note: This statement could instead be the following:
// Node n = (Node)mn;
// However, the compiler doesn't generate a cast because
// it isn't required.
n.setData("Hello"); // Causes a ClassCastException to be thrown.
Integer x = (Integer)mn.data;
브릿지 메서드의 생성을 보았으니 위의 코드가 어째서 에러가 발생하는지 추론해 볼 수 있습니다.
n.setData("Hello");
구문에서 setData((Integer) data);
가 실행되지만 String
은 Integer
로 캐스트 할 수 없기 때문에 ClassCastException
예외를 발생시킵니다.