generic은 wildcard와 달리 Lower Bounded를 지원하지 않는다.
public <T extends E> Collection<T> copy(Collection<T> target) { } // 이건 되는데
public <T super E> Collection<T> copy(Collection<T> target) { } // 이건 안됨
Type Erasure
generic은 컴파일타임에 타입 안정성을 보장받는 것이다.
Generics were introduced to the Java language to provide tighter type checks at compile time and to support generic programming. To implement generics, the Java compiler applies type erasure
- https://docs.oracle.com/javase/tutorial/java/generics/erasure.html
오라클은 제네릭타입 선언 시 컴파일러에 의해 타입 소거라는 절차를 거친다고 말한다.
문서를 읽어보면 만약 제네릭 타입의 상한이 Object인 경우 해당 타입을 Object로 지정한다고 말한다.
정리하자면 제네릭을 Upper Bounded했을 때, 컴파일러에 의해 타입소거 절차를 거치고 제네릭은 상한 타입으로 변경된다는 것이다.
public class TypeErasure1<T extends Object> {
public T value;
}
public class TypeErasure2<T extends Number> {
public T value;
}
// 위의 코드는 컴파일 시 아래와 같이 변경된다.
public class TypeErasure1 {
public Object value;
}
public class TypeErasure2 {
public Number value;
}
제네릭이 실제로 이렇게 동작하기 때문에 런타임에서 제네릭은 아무 의미가 없다.
따라서 오직 컴파일타임에 타입 안정성을 보장받기 위한 타입 추상화라는 것이다.
만약에 타입 소거가 발생하지 않는다면 어떻게 될까?
나도 확실하게는 모르겠지만 감히 추측해보자면
런타임에 <T extends Object> 를 만나게 되었다. T타입이 Object를 확장하는지 판단하기 위해 T에서 확장한 모든 클래스를 읽어들이며 Object를 확장하는지 확인할 것이다. 런타임 오버헤드가 발생하게 된다. (뇌피셜입니다.)
generic이 super를 지원하지 않는 이유는?
<T super Integer>가 가능하다고 가정해보자. T는 Integer의 모든 부모 타입이 올 수 있다.
Integer의 어떤 부모 클래스가 올 지 모르는 상황에서 안전하게 호출할 수 있는 메서드는? Object의 메서드일 것이다.
따라서 결국 타입 소거가 발생하면 Object로 변경될 것이다.
super 뒤에 어떤 클래스를 선언하든 해당 클래스의 부모타입 중 최상위 클래스는 결국 Object이기 때문에 Object로 변경될 것이다.
T가 Object로 변경이 되면 결국 Integer의 부모클래스가 아닌 모든 클래스를 담을 수 있게되고 type bounded의 의미가 없어지게 된다.
쉽게 풀어 설명하기
만약에 <T extends Number> 라고 제네릭 타입을 upper bounded 했습니다.
T는 Number타입에 저장할 수 있는 모든 타입이 될 수 있습니다.
이 상황에서 T에 대해 안전하게 호출할 수 있는 메서드는 무엇이 있을까요?
Number 클래스에 선언되어 있는 모든 메서드는 안전하게 호출되어 오류없이 동작할 겁니다. T는 Number를 확장했기 때문에 오버라이드된 메서드가 호출되든 Number에 선언된 메서드가 호출되든 할 겁니다.
그래서 타입 소거를 거친 뒤, 타입을 상한선인 Number로 지정하는겁니다.
반대로 <T super Number> 라고 제네릭 타입을 low bounded 할 수 있다고 가정해 봅시다.
T는 Number타입이 저장될 수 있는 모든 타입이 될 수 있습니다.
이 상황에서 T에 대해 안전하게 호출할 수 있는 메서드는? Object의 메서드 일겁니다.
T로 올 수 있는 최상위 클래스가 Object이기 때문입니다.
결국 super 뒤에 어떤 클래스가 오든, 타입 소거를 거친 뒤 제네릭은 Object가 되는겁니다.
아무 의미가 없다는 얘기입니다.
그런데 또 문제가 있습니다.
만약에 타입소거를 거친 뒤의 타입이 Object면 Number의 부모 타입만 올 수 있는 것이 아닙니다.
Integer도 들어갈 수 있습니다.
따라서 타입안정성이 보장되지 않기 때문에 super를 지원하지 않습니다. (이부분도 뇌피셜)
그럼 <? super Integer>는 어떻게, 왜 지원하는걸까?
이부분은 구현보다는 개념에 가까운 설명을 해야 한다.
class Angel { }
class Person extends Angel { }
class Employee extends Person { }
class Employer extends Person { }
이러한 클래스 계층도가 존재한다고 가정한다.
아래의 코드는 제대로 동작할까?
class Main {
public static void main(String[] args) {
insertElementsExtends(new ArrayList<Object>());
}
public static void insertElements(List<? super Person> list) {
list.add(new Object());
list.add(new Angel());
}
}
코드를 직관적으로 보면 문제는 없을 것이다. 런타임까지 갈 수 있다면 말이지..
일단 제네릭은 컴파일타임에 타입 안정성을 보장하기 위해 존재하는 것이다. 라는 걸 머리에 박아두고 가야한다.
위의 코드는 컴파일 오류가 발생한다. 왜일까?
<? super Person>
은 Person의 모든 부모클래스가 올 수 있으니 결국은 Object로 변경될 거고 Object 타입으로 캐스팅 될 수 있는 모든 객체가 들어갈 수 있는 것 아닌가? 라고 생각했었는데 이렇게 직관적으로 동작하는 것이 아니었다.
나도 이해하는 게 어려워서 나중에 다시 볼 때를 위해 동화느낌으로 설명해 놓았다.
// ? 는 Person의 부모 타입으로 지정될 수 있는 클래스들이야. (Person, Angel, Object)
// 근데 어떤 부모 타입이 올지 몰라.
// 부모클래스라고 해서 리스트에 추가할 수 없어. 아직 리스트의 원소가 무슨 타입인지 모르거든.
// 내가 리스트에 Object객체를 넣었는데,
// 파라매터를 받고 보니 List<Angel>이면 문제가 발생하잖아?
// 그러니까 최소한 Person이 될 수 있는 클래스를 넣어야 해.
// 그럼 최소한 문제가 발생하진 않을 거거든
public static void insertElementsSuper(List<? super Person> list) {
list.add(new Employee()); // 가능
list.add(new Person()); // 가능
list.add(new Angel()) // 불가능 List<Person>이 올 가능성이 있기 때문
}
그럼 반대로 extends의 상황을 살펴보자.
class Main {
public static void main(String[] args) {
insertElementsExtends(new ArrayList<Person>());
}
public static void insertElements(List<? extends Person> list) {
list.add(new Empolyer());
list.add(new Employee());
list.add(new Person());
}
}
이것도 코드를 직관적으로 보면 문제가 없어보인다.
하지만 이 코드도 컴파일 오류가 발생한다.
<? extends Person>
은 Person타입으로 캐스팅될 수 있는 클래스가 올 수 있으니 문제 없는 거 아니냐? 라고 생각을 했었다. 하지만 이 또한 잘못된 생각이었다는 것..
// ? 는 Person 타입으로 지정될 수 있는 클래스들이야. (Person, Employee, Employer)
// 근데 나는 어떤 자식 타입이 올지 몰라.
// 자식 클래스라고 해서 리스트에 추가할 수 없어, 아직 리스트의 원소가 무슨 타입인지 모르거든.
// 내가 Person객체 를 넣었는데
// 파라매터를 받고 보니 List<Employee>이면 문제가 발생하잖아?
// 그러니까 원소를 추가할 수 없어.
// 제일 자식인 Employee를 넣으면 된다고? 타입이 Employer면 문제가 발생할 수 있어.
// 그러니까 안정성을 위해 원소를 추가하는 것은 지원하지 않을 거야.
public static void insertElementsExtends(List<? extends Person> list) {
list.add(new Person()) // Complie error
list.add(new Employee()) // Complie error
}
? 를 가지는 메서드의 입장에서 생각하면 타입 안정성을 지키기 위한 범위 지정이라는 것이 눈에 보일 것이다.
어떻게 지원하는지는 찾지 못하였다. 레퍼런스가 너무 없는 것 같다.
PE-CS라고 이펙티브자바에서 설명하는 공식이 있다던데 나중에 취업하면 정독하며 해당 포스팅도 갈아엎을 예정이다.
Reference
'의문과 실험' 카테고리의 다른 글
회원 테이블의 PK를 Long 타입에 매핑하는 이유 (0) | 2024.02.04 |
---|---|
메인 메서드에 대한 고찰 (0) | 2024.01.03 |
Monitor와 Synchronized 동작 알아보기 (1) | 2024.01.01 |
상속 시, 오버라이딩된 메서드의 접근제어자는 왜 확장만을 허용할까 (0) | 2023.12.19 |
[Spring] 프로젝트에서 IO를 줄여 성능을 개선해보자 (0) | 2023.10.04 |