[Effective Java] Item 28. 배열보다는 리스트를 사용하라
배열 ↔ 제네릭
1. 배열은 공변, 제네릭은 불공변이다.
배열은 공변이다.
즉, Sub 클래스가 Super 클래스의 하위 타입이면, 배열 Sub[]은 배열 Super[]의 하위 타입이 된다.
Object[] objectArray = new Long[1];
objectARray[0] = "타입이 달라 넣을 수 없다."; // ArrayStoreException을 던진다.
그러므로 이 코드는 컴파일은 성공하지만 런타임 시점에 실패한다.
제네릭은 불공변이다.
서로 다른 타입 Type1과 Type2이 있을 때, List<Type1>과 List<Type2>는 상위 타입도, 하위 타입도 아니다.
List<Object> ol = new ArrayList<Lonb>(); // 호환되지 않는 타입
ol.add("타입이 달라 넣을 수 없다.");
이 코드는 컴파일이 아예 되지 않는다. 보통은 런타임보다 컴파일 시점에 에러를 알아채는 것이 훨씬 편할 것이다.
2. 배열은 실체화(reify)되지만, 제네릭은 소거된다.
배열은 실체화된다.
- 런타임에도 자신이 담기로 한 원소의 타입을 인지하고 확인한다.
- 위 코드처럼 Long 배열에 String을 넣으려고 하면 ArrayStoreException이 발생한다.
제네릭은 소거된다.
- 제네릭은 타입 정보가 런타임에는 소거된다.
- 원소 타입을 컴파일 타임에만 검사하며 런타임에는 알 수조차 없다.
이러한 차이로 인해 배열과 제네릭은 잘 어우러지지 못한다. 예컨대 배열은 제네릭 타입, 매개변수화 타입, 타입 매개변수로 사용할 수 없다. 즉, 코드를 new List<E>[], new List<String>[], new E[] 식으로 작성하면 컴파일 할 때 오류를 일으킨다.
제네릭 배열 생성을 막은 이유
타입이 안전하지 않기 때문이다. 이를 허용하면 컴파일러가 자동 생성한 형변환 코드에서 런타임에 ClassCastException이 발생할 수 있다. 런타임에 ClassCastException이 발생하는 일을 막아주겠다는 제네릭 타입 시스템의 취지에 어긋나는 것이다.
제네릭 타입의 배열 생성이 가능하다고 가정하고 다음 코드를 보자.
List<String>[] stringLists = new List<String>[1];
List<Integer> intList = List.of(42);
Object[] objects = stringLists;
objects[0] = intList;
String s = stringLists[0].get(0);
마지막 줄에서 String 변수에 Integer이 들어가면서 ClassdCastException이 발생하고 만다.
실체화 불가 타입
- E, List<E>, List<String>와 같은 타입
- 실체화되지 않아서 런타임에는 컴파일타임보다 타입 정보를 적게 가진다.
- 매개변수화 타입 가운데 실체화될 수 있는 타입: List<?>와 Map<?, ?> 같은 비한정적 와일드카드 타입
배열을 제네릭으로 만들 수 없어 귀찮은 경우
1. 제네릭 컬렉션에서 자신의 원소 타입을 담은 배열을 반환하는 것이 불가능하다.
2. 제네릭 타입과 가변인수 메서드를 함께 쓰면 해석하기 어려운 경고 메시지를 받는다.
배열로 형변환할 때 제네릭 배열 생성 오류나 비검사 형변환 경고가 뜨는 경우, 대부분 배열인 E[] 대신 컬렉션인 List<E>를 사용하면 해결된다. 코드가 조금 복잡해지고 성능이 살짝 나빠질 수 있지만, 대신 타입 안전성과 상호운용성은 좋아진다.
예로 생성자에서 컬렉션을 받는 Chooser 클래스를 살펴보자. Chooser 클래스는 컬렉션 안의 원소 중 하나를 무작위로 선택해 반환하는 choose 메서드를 제공한다.
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) {
choiceArray = (T[]) choices.toArray();
}
// chose 메서드는 그대로다.
}
이 코드는 실행은 되나, 아래 코드에서 경고가 뜬다.
choiceArray = (T[]) choice.toArray();
T가 무슨 타입인지 알 수 없으니 컴파일러는 이 형변환이 런타임에도 안전한지 보장할 수 없다는 메시지가 뜬다. 이 비검사 형변환 경고는 배열 대신 리스트를 쓰면 안전하게 제거할 수 있다. @SafeVarargs 어노테이션을 이용해 코드가 안전하다고 표시하는 방식으로도 제거할 수는 있다.
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()));
}
}
이제 오류나 경고 없이도 컴파일이 된다. 이전보다 코드양이 조금 늘고 조금 더 느리지만, 런타임에 ClassCastException을 만날 일이 없어진다.
참고 자료: