배열은 공변, 제네릭은 불공변이라 한다. 공변이란 무엇인가?
공변은 Sub가 Super의 하위 타입이라면 해당 타입 즉 배열에서도 Sub[]가 Super[]의 하위 타입이 된다. 즉, 이전 특성이 타입이 변해도 공변이라면 동일하게 특징을 가져온다는 특성이 있다.
제네릭은 List<type1> 와 List<type2> 모두 전혀 다른 타입이다. 상위, 하위 타입과도 전혀 관계가 없다.
제네릭의 특성은 컴파일시에 비교를 하고 매개변수가 소멸한다. 그 이후 런타임에서는 따로 검사를 하지 않는다. 뭐든지 오류는 런타임보다는 컴파일 시점에서 파악하는 것이 오류를 해결하는데 훨씬 도움이 된다. 그러나 배열의 공변 특성 때문에 컴파일에서 타입이 잘못되도 잡을 수 없다.
// 런타임 실패 코드
Object[] objectArray = new Long[1];
objectArray[0] = "hello"; // throw ArrayStoreException
배열로 선언한 경우 컴파일러는 이를 잡을 수 없다. 타입은 Object로 정의 했으나 실제 생성되는 배열의 타입은 Long이다. Long[]은 Object[]의 서브타입이기 때문에 컴파일 단계에서는 문제가 없다. 그리고 "hello"라는 문자열타입이 입력되고 나서야 런타임에 저장할수 없다는 예외가 발생한다.
// 컴파일 실패 코드
List<Object> ol = new ArrayList<Long>(); // 타입이 다르므로 컴파일 실패
ol.add("hello");
제네릭은 비공변이다. Long은 Object의 하위 타입이지만 List<Long>은 List<Object>의 하위타입이 아니다. 그렇기에 타입이 다르고 컴파일에서 오류가 발생한다.
제네릭에서는 이를 해결하기 위해 비한정적 와일드카드 타입을 지원한다.
만약 Object의 하위 타입을 담고 싶다면 다음과 같이 정의하면 된다.
// 비한정적 와일드카드 타입
List<? extends Object> ol = new ArrayList<Long>();
Object[]를 쓰면 형변환을 해야하고 타입에 대한 여러 경고가 뜰 수 있다. 이를 제네릭을 활용하면 효과적으로 없앨 수 있다.
// 제네릭을 적용해야한다.
public class Chooser {
private final Object[] choiceArray;
public Chooser(Collection choices) {
choiceArray = choices.toArray();
}
}
// 컴파일 에러
public class Chooser {
private final T[] choiceArray;
public Chooser(Collection<T> choices) {
choiceArray = (T[])choices.toArray();
}
그러나 이 코드는 경고를 발생한다. 바로 (T[])로 형변환 하는 부분인데 해당 형변환이 런타임에도 안전한지 보장할 수 없기 때문이다. 주석을 남기고 애너테이션을 적용해서 경고를 숨기는 방법도 있지만 사람의 개인적인 판단은 100% 안전하다고 보장할 수 없다. 이럴 때 배열대신 리스트를 사용하면 쉽게 해결한다.
public class Chooser {
private final List<T> choices;
public Chooser(Collection<T> choices) {
choices = new ArrayList<>(choices);
}
}
List는 분명 배열보다 성능이 좋지 않을 수 있다. 기본적으로 클래스이고 배열에 비해 약간 무겁기 때문이다. 그러나 런타임에 ClassCastException에 대해서는 안전함을 100% 보장한다. 성능을 조금 희생하더라도 리스트를 사용하는 것은 확실히 가치가 있다.