우리가 제네릭 클래스를 만들 때, extends
키워드를 통해 다음과 같은 클래스를 만들 수 있다.
class Sample<T extends Number> {
...
}
이렇게 타입을 제한함으로써 이 클래스에 타입 변수 T
는 Number
클래스를 포함한 자식 클래스들만 지정 가능하게 된다. 그런데 다음과 같이 super
를 이용해 타입을 지정하는 것은 불가능하게 되어 있다.
class Sample<T super Number> {
...
}
제네릭 메서드도 마찬가지다. 두 경우 전부 불가능하다.
public static <T super Number> void sample1(T something) {
...
}
public static <T super Number> void sample2(List<T> list) {
...
}
얼핏 보면 말이 되는 문법이다. 타입 변수 T
는 Number
클래스를 포함한 부모 클래스들만 지정하면 된다. 그러나 자바 컴파일러가는 이를 허용하지 않는다. 왜 그럴까?
Type Erasure(타입 소거)란, 자바 코드를 컴파일할 때 타입을 검사하고, 런타임 시에 해당 타입을 삭제하는 절차로 말할 수 있다. 이로 인해 컴파일 시에 타입 안정성을 보장받을 수 있다.
제네릭 클래스에서 타입 소거는 다음과 같이 이루어진다.
class Sample<T> {
T something;
...
}
// 런타임
class Sample {
Object something;
...
}
타입의 제한을 두지 않으면 타입 T
에는 모든지 올 수 있으므로 런타임에 자바 컴파일러가 해당 타입을 Object
타입으로 바꾼다.
class Sample<T extends Number> {
T something;
...
}
// 런타임
class Sample {
Number something;
...
}
extends
를 통해 타입에 상한 제한을 둔다면 해당 타입을 그 타입으로 바꾼다.
제네릭 메서드에서의 타입 소거도 똑같이 이루어진다.
public static <T> void sample(T something) {
...
}
// 런타임
public static void sample(Object something) {
...
}
extends
키워드도 마찬가지다.
public static <T extends Number> void sample(T something) {
...
}
// 런타임
public static void sample(Number something) {
...
}
다음과 같이 메서드의 인자로 제네릭 클래스의 객체를 받는 경우에는 어떨까?
public static <T> void sample(List<T> list) {
...
}
public static <T> void sample2(List<T extends Number> list) {
...
}
// 런타임
public static void sample(List list) {
...
}
public static void sample2(List list) {
...
}
보는 것과 같이 완전히 제네릭 키워드가 사라진다. 와일드 카드를 사용한 경우도 마찬가지다.
public static void sample1(List<?> list) {
...
}
public static void sample2(List<? extends Number> list) {
...
}
public static void sample2(List<? super Number> list) {
...
}
// 런타임
public static void sample1(List list) {
...
}
public static void sample2(List list) {
...
}
public static void sample3(List list) {
...
}
이는 자바의 제네릭의 목적을 다시 한번 복기하면 이유를 알 수 있다. 제네릭은 컴파일시에 타입 체크를 하는 기능이다. 따라서 자바 컴파일러는 컴파일시에 타입 체크를 해서 해당 문법이 유효한지 검사하고 완료되면 코드를 평범한 자바 바이트 코드로 변경한다.
다음과 같은 경우를 생각해보자.
class Sample<T> {
T item;
}
타입 T
는 컴파일 후에 다음과 같이 바뀔 것이다.
class Sample {
Object item;
}
그렇다면 super
키워드의 경우는 어떨까?
class Sample<T super Number> {
T item;
}
타입 T
는 무엇으로 바뀌어야 할까? 우선 T
에는 Number
클래스의 부모들만 올 수 있으므로 결국 최대 Object
클래스가 올 수 있을 것이다. 그럼 위 코드는 컴파일 후 다음과 같이 바뀔 것이다.
class Sample {
Object item;
}
이렇게 하지 않으면, 타입 T
자리에 Object
클래스가 올 수 없다. 그런데 이 코드는 결국 다음 코드들을 컴파일 한 결과와 같다.
class Sample1<T> {
T item;
}
class Sample2<T extends Object> {
T item;
}
메서드 또한 마찬가지다.
public static <T super Number> void sample1(T something) {
...
}
public static <T super Number> void sample2(List<T> list) {
T item;
...
}
위와 같은 경우도 역시 T
에 Number
클래스의 부모들만 올 수 있으므로 최대 Object
클래스가 올 수 있다. 그렇다면 다음과 같이 코드가 바뀐다.
public static void sample1(Object something) {
...
}
public static void sample2(List list) {
Object item;
...
}
이렇게 되면 다음 코드들의 컴파일 한 결과와 같아진다.
public static <T> void sample1(T something) {
...
}
public static <T extends Object> void sample1(T something) {
...
}
public static <T extends Object> void sample2(List<T> list) {
T item;
...
}
이에 따라 super
키워드로 타입을 지정하는 것은 의미가 없게 된다. 그래서 자바는 super
키워드로 타입을 지정하는 것을 허용하지 않는다.
다만, super
키워드로 타입을 지정하지 않고 메서드의 인자에서 와일드 카드를 사용하는 것은 가능하다. 와일드 카드는 타입을 지정하지 않고 컴파일 시에 바운더리만 검사하기 때문이다.
public static void sample(List<? super Number> list) {
...
}
후에 위 코드는 아래와 같이 바뀐다.
public static void sample(List list) {
...
}
https://www.baeldung.com/java-type-erasure
https://stackoverflow.com/questions/58575372/how-does-java-type-erasure-treats