제네릭에서 T super ... 사용이 불가능한 이유 Generic-Type Erasure

lsy·2022년 10월 25일
0

extends와 super

우리가 제네릭 클래스를 만들 때, extends 키워드를 통해 다음과 같은 클래스를 만들 수 있다.

class Sample<T extends Number> {
	...
}

이렇게 타입을 제한함으로써 이 클래스에 타입 변수 TNumber 클래스를 포함한 자식 클래스들만 지정 가능하게 된다. 그런데 다음과 같이 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) {
	...
}

얼핏 보면 말이 되는 문법이다. 타입 변수 TNumber 클래스를 포함한 부모 클래스들만 지정하면 된다. 그러나 자바 컴파일러가는 이를 허용하지 않는다. 왜 그럴까?

Generic-Type Erasure

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) {
	...
}

이는 자바의 제네릭의 목적을 다시 한번 복기하면 이유를 알 수 있다. 제네릭은 컴파일시에 타입 체크를 하는 기능이다. 따라서 자바 컴파일러는 컴파일시에 타입 체크를 해서 해당 문법이 유효한지 검사하고 완료되면 코드를 평범한 자바 바이트 코드로 변경한다.

그렇다면 super로 타입 지정이 안되는 이유?

다음과 같은 경우를 생각해보자.

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;
    ...
}

위와 같은 경우도 역시 TNumber클래스의 부모들만 올 수 있으므로 최대 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) {
	...
}


Reference

https://www.baeldung.com/java-type-erasure
https://stackoverflow.com/questions/58575372/how-does-java-type-erasure-treats

profile
server를 공부하고 있습니다.

0개의 댓글