배열과 제네릭에는 중요한 차이가 두 가지 있다. 공변(variant)과 실체화(reify)이다.
List<Type1>
은 List<Type2>
의 하위 타입도 아니고 상위 타입도 아니다.class Temp {
public static void main(String[] args) {
Object[] objectArray = new Long[1];
objectArray[0] = "타입이 달라 넣을 수 없다." // ArrayStoreException을 던진다.
}
}
다음 코드는 문법에 맞지 않는다.
class Temp {
public static void main(String[] args) {
List<Object> ol = new ArrayList<Long>();
ol.add("타입이 달라 넣을 수 없다.");
}
}
이와 같은 차이로 인해 배열과 제네릭은 잘 어우러지지 못한다.
new List<E>[]
, new List<String[]>
, new E[]
와 같이 사용하려고 하면 컴파일할 때 제네릭 배열 생성 오류를 일으킨다.import com.sun.tools.javac.util.List;
class Temp {
public static void main(String[] args) {
List<String>[] stringLists = new List<String>[1]; // (1)
List<Integer> intList = List.of(42); // (2)
Object[] objects = stringLists; // (3)
objects[0] = intList; // (4)
String s = stringLists.get(0); // (5) 컴파일 에러가 난다!
}
}
컴파일 에러가 나는 과정은 다음과 같다.
List<Integer>
를 생성한다.List<String>
의 배열을 Object 배열에 할당한다. 이때, 배열은 공변이므로 아무런 문제가 없다.List<Integer>
의 인스턴스를 Object 배열의 첫 원소로 저장한다. 이때, 제네릭은 소거 방식으로 구현되어서 이 역시 성공한다.List<Integer>
인스턴스의 타입은 단순히 List
가 되고, List<Integer>[]
인스턴스의 타입은 List[]
가 된다.List<String>
인스턴스만 담겠다고 선언한 stringLists 배열에는 현재 List<Integer>
인스턴스가 저장되어 있다.E
, List<E>
, List<String>
같은 타입을 실체화 불가 타입(non-reifiable type)이라 한다.
List<?>
와 Map<?,?>
같은 비한정적 와일드카드 타입뿐이다.제네릭 컬렉션에서는 자신의 원소 타입을 담은 배열을 반환하는 게 보통은 불가능하다.
제네릭 타입과 가변인수 메서드(varargs method)를 함께 쓰면 해석하기 어려운 경고 메시지를 받게 된다.
배열로 형변환할 때제네릭 배열 생성 오류나 비검사 형변환 경고가 뜨는 경우 대부분은 배열인 E[]
대신 컬렉션인 List<E>
를 사용하면 해결된다.
코드가 조금 복잡해지고 성능이 살짝 나빠질 수도 있지만, 대신 타입 안전성과 상호운용성은 좋아진다.
public class Chooser {
private final Object[] choiceArray;
public Chooser(Collection choices) {
this.choiceArray = choices.toArray();
}
public Object choose() {
ThreadLocalRandom random = ThreadLocalRandom.current();
return choiceArray[random.nextInt(choiceArray.length)];
}
}
public class Chooser {
private final T[] choiceArray;
public Chooser(Collection<T> choices) {
this.choiceArray = choices.toArray();
}
// ...
}
public class Chooser {
private final T[] choiceArray;
public Chooser(Collection<T> choices) {
this.choiceArray = (T[]) choices.toArray();
}
// ...
}
public class Chooser<T> {
private final List<T> choiceList;
public Chooser(Collection<T> choices) {
choiceList = new ArrayList<>(choices);
}
public T choose() {
ThreadLocalRandom random = ThreadLocalRandom.current();
return choiceList.get(random.nextInt(choiceList.size()));
}
}