1. 배열은 공변(covariant)인 반면, 제네릭은 불공변(invariant)이다.
Sub가 Super의 하위 타입이라면 배열 Sub[]는 배열 Super[]의 하위 타입이 되지만, List<Sub>은 List<Super>의 하위 타입도 아니고 상위 타입도 아니다.
2. 배열은 실체화(reify)된다.
배열은 런타임에도 원소의 타입을 확인한다. 즉, 런타임에 예외가 발생할 수 있다.
반면, 제네릭은 타입 정보가 런타임에는 소거된다. 원소 타입을 컴파일타임에만 검사하며 런타임에는 알 수조차 없다.
💡 위와 같은 차이로 인해, 배열과 제네릭은 잘 어우러지지 못한다.
예를 들어, 배열은 제네릭 타입(new List<E>[]
), 매개변수화 타입(new List<String>[]
), 타입 매개변수(new E[]
)로 사용할 수 없다.
생성자에서 컬렉션을 받고, 컬렉션 안의 원소 중 하나를 무작위로 선택해 반환하는 choose
메소드를 제공하는 Chooser
클래스를 구현해보자.
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)];
}
}
이 클래스를 사용하려면 choose 메서드를 호출할 때마다 반환된 Object를 원하는 타입으로 형변환해야 한다.
이때 만약 다른 타입의 원소가 들어 있었다면 런타임에 형변환 오류가 발생할 것이다.
따라서 이 클래스는 제네릭 타입으로 만들어야 한다.
public class Chooser<T> {
private final T[] choiceArray;
public Chooser(Collection<T> choices) {
// 1. 컴파일 오류
// choiceArray = choices.toArray();
// 2. 경고
choiceArray = (T[]) choices.toArray();
}
public Object choose() {
Random rnd = ThreadLocalRandom.current();
return choiceArray[rnd.nextInt(choiceArray.length)];
}
}
1번
과 같이 작성할 경우, 배열 Object[]
이 배열 T[]
과 타입이 맞지 않아 컴파일 오류가 발생한다.
이를 해결하기 위해 2번
과 같이 작성한 경우, 오류는 발생하지 않지만 런타임에 형변환이 안전한지 보장할 수 없다는 경고 메세지가 나온다.
이 코드의 경우, 동작하기는 하지만 컴파일러가 안전을 보장하지 못 한다.
비검사 형변환 경고를 제거하려면 배열 대신 리스트를 쓰면 된다.
다음 코드는 오류나 경고 없이 컴파일된다.
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()));
}
}
📌 핵심 정리
- 배열은 공변이고 실체화되는 반면, 제네릭은 불공변이고 타입 정보가 소거된다.
- 배열은 런타임에는 타입 안전하지만 컴파일타임에는 그렇지 않다.
제네릭은 그 반대다.- 이러한 이유로 배열과 제네릭을 섞어 쓰기란 쉽지 않다.
- 둘을 섞어 쓰다가 컴파일 오류나 경고를 만나면, 가장 먼저 배열을 리스트로 대체하는 방법을 적용해보자.