포스트

제네릭 공식문서 파해치기 (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);
    }
}

위에서 MyNodeNode를 상속받아 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);
    }

    // ...
}

위에서 작성한 타입 소거 전 예시에서는 TInteger가 되었기 때문에 브릿지 메서드 (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); 가 실행되지만 StringInteger로 캐스트 할 수 없기 때문에 ClassCastException 예외를 발생시킵니다.


이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.