배열과 제네릭에는 매우 다른 타입 규칙이 적용된다. 배열은 공변이고 실체화되는 반면, 제네릭은 불공변이고 타입 정보가 소거된다. 그 결과 배열은 런타임에는 타입 안전하지만 컴파일타임에는 그렇지 않다. 제네릭은 반대다. 그래서 둘을 섞어 쓰기란 쉽지 않다. 둘을 섞어 쓰다가 컴파일 오류나 경고를 만나면, 가장 먼저 배열을 리스트로 대체하는 방법을 적용해보자.
배열과 제네릭의 중요한 차이1. 배열은 covariant(공변), 제네릭은 invariant(불공변)2. 배열은 실체화됨(reify)제네릭 배열을 만들지 못하게 막은 이유? → 타입 안전하지 않기 때문배열 → 제네릭으로 수정하는 예시
배열과 제네릭의 중요한 차이
1. 배열은 covariant(공변), 제네릭은 invariant(불공변)
- covariant
- Sub가 Super의 하위타입이라면 Sub[]는 Super[]의 하위 타입이 된다(공변, 즉 함께 변함)
- invariant
- 서로 다른 타입 Type1, Type2가 있을 때, List<Type1> 은 List<Type2>의 하위 타입도, 상위 타입도 아님
Object[] objectArray = new Long[1]; objectArray[0] = "타입이 달라 넣을 수 없다."; // ArrayStoreException을 던짐
List<Object> ol = new ArrayList<Long>(); // 호환되지 않는 타입 ol.add("타입이 달라 넣을 수 없다.");
- 배열에서는 그 실수를 런타임에야 알게 되지만, 리스트를 사용하면 컴파일할 때 바로 알 수 있음
2. 배열은 실체화됨(reify)
- 의미 : 배열은 런타임에도 자신이 담기로 한 원소의 타입을 인지하고 확인한다.
- 반면, 제네릭은 타입 정보가 런타임에는 소거됨(type erasure)
- 원소 타입을 컴파일 타임에만 검사하며, 런타임에는 알수조차 없다는 뜻임
- 소거는 제네릭이 지원되기 전의 레거시 코드와 제네릭 타입을 함께 사용할 수 있게 해주는 메커니즘임
위의 주요 차이로 인해 배열과 제네릭은 잘 어우러지지 못한다. 예컨대 배열은 제네릭 타입, 매개변수화 타입, 타입 매개변수로 사용할 수 없다. 즉, 코드를 new List<E>[], new List<String>[], new E[] 식으로 작성하면 컴파일할 때 제네릭 배열 생성 오류를 일으킴
제네릭 배열을 만들지 못하게 막은 이유? → 타입 안전하지 않기 때문
- 이를 허용하면 컴파일러가 자동 생성한 형변환 코드에서 런타임에 ClassCastException 이 발생할 수 있다.
- E, List<E>, List<String> 같은 타입을 실체화 불가 타입(non-reifiable type)이라 함. 쉽게 말해, 실체화되지 않아서 런타임에는 컴파일타임보다 타입 정보를 적게 가지는 타입임
- 소거 메커니즘 때문에 매개변수화 타입 가운데 실체화될 수 있는 타입은 List<?>와 Map<?,?> 같은 비한정적 와일드카드 타입 뿐임
배열 → 제네릭으로 수정하는 예시
public class Chooser { private final Object[] choiceArray; public Chooser(Collection choices) { choiceArray = choices.toArray(); } public Object choose() { Random rnd = ThreadLocalRandom.current(); return choiceArray[rnd.nextInt(choiceArray.length)]; } }
public class Chooser<T> { private final T[] choiceArray; public Chooser(Collection<T> choices) { choiceArray = choices.toArray(); // 오류 발생 /* choiceArray = (T[]) choices.toArray(); 로 수정 */ } }
// 코드 28-6 리스트 기반 Chooser - 타입 안전성 확보! (168쪽) public class Chooser<T> { private final List<T> choiceList; public Chooser(Collection<T> choices) { choiceList = new ArrayList<>(choices); } public T choose() { Random rnd = ThreadLocalRandom.current(); return choiceList.get(rnd.nextInt(choiceList.size())); } public static void main(String[] args) { List<Integer> intList = List.of(1, 2, 3, 4, 5, 6); Chooser<Integer> chooser = new Chooser<>(intList); for (int i = 0; i < 10; i++) { Number choice = chooser.choose(); System.out.println(choice); } } }