[effective java] 아이템 28. 배열보다는 리스트를 사용하라.
배열 | 제네릭 |
공변(Sub가 Super의 하위 타입이라면 배열 Sub[]는 배열 Super[]의 하위 타입이 된다.) | 불공변(서로 다른 타입 Type1과 Type2가 있을 때, List은 List의 하위 타입도 아니고 상위 타입도 아니다.) |
런타임 단계에서 발견 | 컴파일 단계에서 발견 |
실체화(런타임에도 자신이 담기로 한 원소의 타입을 인지하고 확인) | 실체화 x(원소 타입을 컴파일 타임에만 검사하여 런타임에는 알 수 없음) |
제네릭 타입, 매개변수화 타입, 타입 매개변수 사용 불가(cuz Type Safety) |
실체화 타입
- 컴파일 타임과 런타임에 동일한 타입 정보를 가지는 타입
- 반대로 실체화 불가 타입은 런타임에는 타입 정보가 소거되어 컴파일 타임보다 타입 정보를 적게 가지는 타입
실체화, 소거
String[] stringArray = new String[] { "실체화 타입" };
List<String> stringLIst = Arrays.asList( "실체화 불가 타입" );
공변
Object[] objectArray = new Long[1];
objectArray[0] = "타입이 달라 넣을 수 없다.";
불공변
List<Object> ol = new ArrayList<Long>(); // Object만 넣을 수 있음, <>로 수정하면 됨
ol.add("타입이 달라 넣을 수 없다.");
소거(type erasure)
제네릭이 처음 도입됐을 때, 하위 호환성 문제를 해결하기 위해 도입된 메커니즘이다. 제네릭이 도입되기 전에 작성된 코드와 제네릭을 함께 사용할 수 있게 해주는 역할을 한다.
제네릭이 지원되기 전의 코드는 모두 Raw Type으로 처리된다. 따라서 컴파일러는 제네릭을 사용하는 코드에서 타입 파라미터의 정보를 소거하고, 로 타입으로 대체한다. 즉, 컴파일 시점에는 제네릭 타입 정보가 모두 사라지고, 로 타입으로 변환된다.
소거를 통해 제네릭 타입이 로 타입으로 변환되기 때문에, 제네릭 타입을 사용하는 코드는 제네릭 타입의 실제 타입 인자를 알 수 없게 된다. 따라서 제네릭 타입의 실제 타입 인자에 대한 정보를 런타임 시점에서는 사용할 수 없게 된다.
제네릭 배열 생성이 안되는 이유
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[0].get(0);// (5)
}
제네릭 배열을 생성하는 (1)이 허용된다고 가정하자. (3)은 (1)에서 생성한 List<String>을 원소로 담고 있는 배열을 Object[]에 할당한다. List<String> 또한 Object이므로 할당할 수 있다. 즉, 런타임에는 List<Integer> 인스턴스 타입은 List, List<Integer>[] 인스턴스 타입은 List[]가 된다. 따라서 ArrayStoreException이 발생하지 않는다. (5)에서 'ClassCastException'이 발생한다.
결국 List<String> 타입의 인스턴스만 담겠다는 의도가 있지만, 실제로는 List<Integer> 타입의 인스턴스도 담을 수 있게 되므로, 제네릭 타입 안정성이 깨지게 된다. 따라서 제네릭 배열 생성을 허용하지 않는다.
배열보다 리스트를 권장하는 이유
아래 코드는 choiceArray 배열의 랜덤한 원소를 반환한다.
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)];// 형변환 오류 발생 가능
}
}
이 코드에서는 Collection의 제네릭 타입이 명시되어 있지 않고, 로 타입을 사용한다. 그러나 toArray() 메소드에서는 제네릭 타입 정보를 보존하지 않는다 따라서 choiceArray는 Object 배열로 만들어지며, choose() 메소드에서 해당 배열의 원소를 반환할 때 형변환 오류가 발생할 수 있다.
public class Chooser<T> {
private final T[] choiceArray;
public Chooser(Collection<T> choices) {
choiceArray = choices.toArray();// Object[] cannot be converted to T[]
}
public Object choose() {
Random rnd = ThreadLocalRandom.current();
return choiceArray[rnd.nextInt(choiceArray.length)];
}
}
위 코드는 형 변환 오류를 막기 위해 제네릭 클래스로 변경한 코드이다. 그러나 컴파일 에러가 뜨며, Object[]를 T[]로 형 변환하여도 Unchecked cast 경고가 뜬다. 물론, 개발자가 안전하다고 확신할 수 있다면 주석과 에너테이션을 달아 경고를 숨길 수 있으나, 원인을 제거하는 것이 좋다.
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()));
}
}
위는 배열 대신 리스트를 사용하여 원인을 제거한 코드이다. 즉, 배열로 형변환할 때 제네릭 배열 생성 오류나 비검사 형변환 경고가 뜨는 경우 대부분은 배열인 E[] 대신 컬렉션인 List<E>를 사용하면 해결된다.