Item 28. 배열보다는 리스트를 사용하라
배열과 제네릭 타입의 차이
- 배열은 공변(convariant)이다. Sub가 Super의 하위 타입이라면 배열 Sub[]는 배열 Super[]의 하위 타입이 된다(공변, 즉 함께 변한다는 뜻이다). 반면, 제네릭은 불공변(invariant)이다. 즉, List<Type1>은 List<Type2>의 하위 타입도 아니고 상위 타입도 아니다.
- 배열은 실체화(reify)된다. 배열은 런타임에도 자신이 담기로 한 원소의 타입을 인지하고 확인한다(아래 코드 28-1 런타임 실패). 반면, 제네릭은 타입 정보가 런타임에는 소거(erasure)된다. 원소 타입을 컴파일타임에만 검사한다. 소거는 제네릭이 지원되기 전의 레거시 코드와 제네릭 타입을 함께 사용할 수 있게 해주는 메커니즘이다.
// 코드 28-1 런타임에 실패한다.
Object[] objectArray = new Long[1];
objectArray[0] = "타입이 달라 넣을 수 없다."; // ArrayStoreException을 던진다.
// 코드 28-2 컴파일되지 않는다.
List<Object> ol = new ArrayList<Long>(); // 호환되지 않는 타입이다.
ol.add("타입이 달라 넣을 수 없다.");
어느 쪽이든 Long용 저장소에 String을 넣을 수는 없습니다. 다만 배열에서는 그 실수를 런타임에야 알게 되지만, 리스트를 사용하면 컴파일할 때 바로 알 수 있습니다.
배열로 형변환할 때 제네릭 배열 생성 오류나 비검사 형변환 경고가 뜨는 경우, 대부분은 배열인 E[] 대신 컬렉션인 List<E>를 사용하면 해결됩니다. 코드가 조금 복잡해지고 성능이 살짝 나빠질 수도 있지만, 그 대신 타입 안정성과 상호운용성은 좋아집니다.
생성자에서 컬렉션을 받는 Chooser 클래스를 예로 살펴봅시다. 이 클래스는 컬렉션 안의 원소 중 하나를 무작위로 선택해 반환하는 choose 메서드를 제공하며, 생성자에 어떤 컬렉션을 넘기느냐에 따라 주사위판, 매직 8볼, 몬테카를로(Monte Carlo) 시뮬레이션용 데이터 소스 등으로 사용할 수 있습니다.
// 코드 28-3 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를 원하는 타입으로 형변환해야 합니다. 혹시나 타입이 다른 원소가 들어 있었다면 런타임에 형변환 오류가 날 것입니다. 이 클래스를 제네릭으로 만들면 다음과 같습니다.
// 코드 28-4 Chooser를 제네릭으로 변환
public class Chooser<T> {
private final T[] choiceArray;
public Chooser(Collection<T> choices) {
// choiceArray = choices.toArray(); // 컴파일되지 않는다.
choiceArray = (T[]) choices.toArray(); // warning: [unchecked] unchecked cast
}
// choose 메서드는 그대로다.
}
컴파일 실패를 피해 Object 배열을 T배열로 형변환하면 warning이 발생합니다. T가 무슨 타입인지 알 수 없으니 컴파일러는 이 형변환이 런타임에도 안전한지 보장할 수 없다는 메시지입니다.
비검사 형변환 경고를 제거하려면 배열 대신 리스트를 쓰면 됩니다. 다음 Chooser는 오류나 경고 없이 컴파일됩니다.
package effectivejava.chapter5.item28;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Random;
import java.util.concurrent.ThreadLocalRandom;
// 코드 28-5 리스트 기반 Chooser - 타입 안전성 확보!
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);
}
}
}
배열과 제네릭에는 매우 다른 타입 규칙이 적용된다.
배열은 공변이고 실체화되는 반면, 제네릭은 불공변이고 타입 정보가 소거된다.
그 결과 배열은 런타임에는 타입 안전하지만 컴파일타임에는 그렇지 않다.
제네릭은 반대다.
둘을 섞어 쓰다가 컴파일 오류나 경고를 만나면, 가장 먼저 배열을 리스트로 대처하는 방법을 적용해보자.
[참고 정보]
이펙티브 자바 Effective Java 3/E 도서 [조슈아 블로크 저]