배열과 제네릭 타입에는 중요한 차이가 두 가지 있다.
1. 배열은 공변이다. 반면, 제네릭은 불공변이다.
Sub가 Super의 하위 타입이라면 Sub[]
는 Super[]
의 하위 타입이 된다. 반면, 서로 다른 타입 Type1과 Type2가 있을 때, List<Type1>
은 List<Type2>
의 하위 타입도, 상위 타입도 아니다.
Super[] a = new Sub[1];
(O),List<Super> b = new ArrayList<Sub>();
(X)
이것만 보면 제네릭에 문제가 있다고 생각할 수도 있지만 사실 문제가 있는 건 배열 쪽이다. 배열에서는 실수를 런타임에야 알게 되지만, 리스트를 사용하면 컴파일할 때 바로 알 수 있다.
// 런타임에 실패한다.
Object[] objectArray = new Long[1];
objectArray[0] = "타입이 달라 넣을 수 없다."; // ArrayStoreException을 던진다.
// 컴파일되지 않는다!
List<Object> ol = new ArrayList<Long>(); // 호환되지 않는 타입이다.
ol.add("타입이 달라 넣을 수 없다.");
2. 배열은 실체화(reify, 런타임에도 자신이 담기로 한 원소의 타입을 인지하고 확인)된다. 반면, 제네릭은 타입 정보가 런타임에는 소거된다.
이상의 주요 차이로 인해 배열과 제네릭은 잘 어우러지지 못한다.
- 예컨대 배열은 제네릭 타입, 매개변수화 타입, 타입 매개변수로 사용할 수 없다. (
new List<E>[]
,new List<String>[]
,new E[]
)
제네릭 배열을 만들지 못하기 막은 이유는 타입 안전하지 않기 때문이다. 이를 허용한다면 컴파일러가 자동 생성한 형변환 코드에서 런타임에 ClassCastException이 발생할 수 있다.
E
, List<E>
, List<String>
같은 타입을 실체화 불가 타입이라 한다.
- 실체화되지 않아서 런타임에는 컴파일타임보다 타입 정보를 적게 가지는 타입이다.
소거 메커니즘 때문에 매개변수화 타입 가운데 실체화될 수 있는 타입은
List<?>
와Map<?,?>
같은 비한정적 와일드카드 타입뿐이다.
배열을 비한정적 와일드카드 타입으로 만들 수는 있지만, 유용하게 쓰일 일은 거의 없다.참고: 타입 소거를 한다고 해도 잃을 정보가 없기 때문에 실체화 타입이라 볼 수 있는 것이다. 컴파일 시점에 Object로 변환된다. (출처)
제네릭 타입과 가변인수 메서드를 함께 쓰면 해석하기 어려운 경고 메시지를 받게 된다.
- 가변인수 메서드를 호출하면 가변인수 매개변수를 담을 배열이 만들어지는데, 그 배열의 원소가 실체화 불가 타입이라면 경고 발생
배열로 형변환할 때 제네릭 배열 생성 오류나 비검사 형변환 경고가 뜨는 경우 대부분은 배열인 E[]
대신 컬렉션인 List<E>
를 사용하면 해결된다. 코드가 조금 복잡해지고 성능이 살짝 나빠질 수도 있지만, 런타임에 ClassCastException을 만날 일은 없으니 그만한 가치가 있다.