배열과 제네릭 타입에는 중요한 차이가 두 가지 있다.
첫번째, 배열은 공변이다.(공변 - 함께 변한다는 뜻)
Sub가 Super의 하위 타입이라면 배열 Sub[]는 배열 Super[]의 하위 타입이 된다.
반면 제네릭은 불공변이다.
즉, 서로 다른 타입 Type1과 Type2가 있을 때, List<Type1>은 List<Type2>의 하위 타입도, 상위 타입도 아니다.
이것만 보면 제네릭에 문제가 있다고 생각할 수 있지만 문제가 있는 건 배열 쪽이다.
배열은 런타임 시점에, 제네릭은 컴파일 시점에 문제를 발견할 수 있기 때문이다.
두번째, 배열은 실체화된다.
배열은 런타임에도 자신이 담기로 한 원소의 타입을 인지하고 확인한다.
그렇기 때문에 런타임 시점에 문제가 발견되는 것이다.
반면에 제네릭은 타입 정보가 런타임에는 소거된다.
이상의 주요 차이로 인해 배열과 제네릭은 잘 어우러지지 못한다.
예를 들어 배열은 제네릭 타입, 매개변수화 타입, 타입 매개변수로 사용할 수 없다.
즉, 코드를 new List<E>[], new List<String>[], new E[] 와 같이 작성하면 컴파일할 때 제네릭 배열 생성 오류를 일으킨다.
제네릭 배열을 만들지 못하게 막아놓은 이유는 무엇일까?
타입 안전성이 떨어지기 때문이다.
이를 허용한다면 런타임에 ClassCastException이 발생할 수 있다.
런타임에 ClassCastException이 발생하는 일을 막아주겠다는 제네릭 타입 시스템의 취지에 어긋나는 것이다.
E, List<E>, List<String> 같은 타입을 실체화 불가 타입이라 한다.
쉽게 말해, 실체화되지 않아서 런타임에는 컴파일타임보다 타입 정보를 적게 가지는 타입이다.
소거 메커니즘 때문에 매개변수화 타입 가운데 실체화될 수 있는 타입은 List<?>와 Map<?,?> 같은 비한정적 와일드카드 타입뿐이다.
배열을 비한정적 와일드카드 타입으로 만들 수는 있지만, 유용하게 쓰일 일은 거의 없다.
배열을 제네릭으로 만들 수 없어 귀찮을 때도 있다.
예를 들어 제네릭 컬렉션에서는 자신의 원소 타입을 담은 배열을 반환하는 게 보통 불가능하다.
(완벽하지는 않지만 대부분의 상황에서 이 문제를 해결해주는 방법을 아이템 33에서 설명한다.)
또한 제네릭 타입과 가변인수 메서드를 함께 쓰면 해석하기 어려운 경고 메세지를 받게 된다.
이 문제는 @SafeVarargs 애너테이션으로 대처할 수 있다.(아이템 32)
배열로 형변환할 때 제네릭 배열 생성 오류나 비검사 형변환 경고가 뜨는 경우 대부분은 배열인 E[] 대신 컬렉션인 List<E>를 사용하면 해결된다.
코드가 조금 복잡해지고 성능이 살짝 나빠질 수도 있지만, 그 대신 타입 안정성이 좋아진다.
핵심 정리
배열과 제네릭에는 매우 다른 타입 규칙이 적용된다.
배열은 공변이고 실체화되는 반면, 제네릭은 불공변이고 타입 정보가 소거 된다.
그 결과 배열은 런타임에는 타입 안전하지만 컴파일 타임에는 그렇지 않다.
제네릭은 반대다.
그래서 둘을 섞어 쓰기는 어렵다.
둘을 섞어 쓰다가 컴파일 오류나 경고를 만나면, 가장 먼저 배열을 리스트로 대체하는 방법을 적용해보자.
'Book > Effective Java' 카테고리의 다른 글
[Effective Java]30.이왕이면 제네릭 메서드로 만들라 (0) | 2022.07.07 |
---|---|
[Effective Java]29.이왕이면 제네릭 타입으로 만들라 (0) | 2022.07.07 |
[Effective Java]27.비검사 경고를 제거하라 (0) | 2022.07.07 |
[Effective java]26.로 타입은 사용하지 말라 (0) | 2022.07.07 |
[Effective Java]25.톱레벨 클래스는 한 파일에 하나만 담으라 (0) | 2022.07.06 |