배열과 제네릭 타입에는 중요한 차이가 있다
첫째로, 배열은 공변이다. 공변의 의미는 Sub가 Super의 하위 타입이라면 배열[]는 배열 Super[]의 하위 타입이 된다. 반면 제네릭은 불공편이다. 즉, 서로 다른 타입 Type1, Type2가 있을때, List<Type1>
은 List<Type2>
의 하위 타입도 아니고 상위 타입도 아니다.
이것만 보면 리스트가 문제 있어 보일 수 있지만 사실 문제가 있는건 배열쪽이다.
Object[] array = new Long[1];
array[0] = "Hello World"; // ArrayStoreException을 던진다
List<Object> ol = new ArrayList<Long>();
ol.add("Hello World"); // 불가능하다
어느 쪽이든 Long용 저장소에 String을 넣을 수 없다. 다만 배열에서는 그 실수를 런타임에야 알게 되지만 리스트는 런타임시 알수 있다.
둘재로, 배열은 실체화 된다. 무슨뜻이냐 하면 배열은 런타임에도 자신이 담기로 한 원소의 타입을 인지하고 확인한다. 배열은 런타임에도 자신이 담기로 한 원소의 타입을 인지하고 확인한다. 그래서 위의 코드에서 Long 배열에 String을 넣으려 하면 ArrayStoreException이 발생한다. 반면 앞서 이야기했듯 제네릭은 타입 정보가 런타임에는 소거된다. 원소 타입을 컴파일시에만 검사하면 런타임에는 알수조차 없다.
이러한 차이점 때문에 배열과 제네릭은 잘 어우러지지 못한다. 예컨대 배열은 제네릭 타입, 매개변수화 타입 같은것들을 사용할 수 없다.
E, List<E>, List<String>
같은 타입을 실체화 불가 타입(non-reifiable type)이라 한다. 쉽게 말해, 실체화되지 않아서 런타임에는 컴파일타임보다 타입 정보를 적게 가지는 타입이다. 소거 메커니즘 때문에 매개변수화 타입 가운데 실체화될 수 있는 타입은 List<?>와 Map<?, ?>
같은 비한정적 와일드 카드 타입뿐이다.
배열과 제네릭에는 매우 다른 타입 규칙이 적용된다. 배열은 공변이고 실체화되는 반면, 제네릭은 불공변이고 타입 정보가 소거된다. 그 결과 배열은 런타임에는 타입 안전하지만, 컴파일타임에는 그렇지 않다. 제네릭은 반대다. 그래서 둘을 섞어 쓰기란 쉽지 않다.