[Why]
배열과 제네릭 타입의 차이
- 첫 번째
- 배열은 공변(covariant; 共變)이다.
Sub가 Super의 하위 타입이라면 Sub[]는 Super[]의 하위 타입 - 제네릭은 불공변(invariant; 不共變)이다.
서로 다른 타입 Type1과 Type2이 있을 때, List<Type1>은 List<Type2>의 하위 타입도, 상위 타입도 아니다. - 문제는 배열 쪽에 있다.
- 배열은 공변(covariant; 共變)이다.
Object[] objectArray = new Long[1];
objectArray[0] = "타입이 달라 넣을 수 없다."; // ArrayStoreException을 던진다.
위 코드는 문법 상 허용되지만, 런타임에 에러를 발생시킨다.
List<Object> ol = new ArrayList<Long>(); // 호환되지 않는 타입이다.
ol.add("타입이 달라 넣을 수 없다.");
위 코드는 컴파일되지 않는다.
- 두 번째
- 배열은 실체화(reify)된다.
배열은 런타임에도 자신이 담기로 한 원소의 타입을 인지하고 확인한다.
그래서 Long 배열에 String을 넣으려고 하면 ArrayStoreException이 발생한다. - 제네릭은 타입 정보가 런타임에는 소거(erasure)된다.
자바5 이전의 코드와의 호환을 위해서 소거
런타임에는 컴파일타임보다 타입 정보를 적게 가진다.
- 배열은 실체화(reify)된다.
- 이상의 차이로 인해 배열은 제네릭 타입, 매개변수화 타입, 타입 매개변수로 사용할 수 없다.
- new List<E>[], new List<String>[], new E[] : 컴파일 시 제네릭 배열 생성 오류
- 소거 메커니즘 때문에 매개변수화 타입 가운데 실체화될 수 있는 타입은 List<?>와 Map<?,?>같은 비한정적 와일드카드 타입뿐이지만 유용하지는 않다. (아이템26)
제네릭 배열의 생성을 막은 이유
List<String>[] stringLists = new List<String>[1]; // (1)
List<Integer> intList = List.of(42); // (2)
Object[] objects = stringLists; // (3)
Object[0] = intList; // (4)
String s = stringLists[0].get(0); // (5)
(Integer 리스트 배열을 String 리스트 배열의 0번 인덱스에 넣은 후 조회하는 코드)
- 제네릭 배열을 생성하는 (1)이 허용된다고 가정해보자.
- (3)에서 배열은 공변이니 List<String>[]을 Object[]에 넣는 것에 문제가 없다.
- (4)에서 제네릭은 소거 방식으로 구현되어 있기 때문에 List<Integer>의 인스턴스를 Object 배열의 첫 원소로 저장한다.
- 런타임에 List<Integer> 인스턴스 타입은 단순한 List가 되고, List<Integer>[] 타입은 List[]가 된다.
따라서 (4)에서도 ArrayStoreException을 일으키지 않는다. - (5)에서 문제가 발생한다. 컴파일러는 꺼낸 원소를 자동으로 String으로 형변환하는데, 이 원소는 Integer이므로 런타임에 ClassCastException이 발생한다.
[How]
- 배열로 형변환할 때 제네릭 배열 생성 오류나 비검사 형변환 경고가 뜨는 경우 대부분은 배열인 E[] 대신 컬렉션인 List<E>를 사용하면 해결된다.
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)];
}
}
생성자에서 컬렉션을 받는 위의 Chooser 클래스는 컬렉션 안의 원소 중 하나를 무작위로 선택해 반환하는 choose 메서드를 제공한다.
이 클래스를 사용하려면 choose 메서드를 호출할 때마다 반환된 Object를 원하는 타입으로 형변환해야 한다.
혹시나 타입이 다른 원소가 들어 있었다면 런타임에 형변환 오류가 날 것이다.
public class Chooser<T> {
private final T[] choiceArray;
public Chooser(Collection<T> choices) {
choiceArray = choices.toArray();
}
// choose 메서드는 그대로
}
클래스를 제네릭으로 바꿔봤다.
하지만 이 클래스를 컴파일하면 다음의 오류가 발생한다.
Chooser.java:9: error: incompatible types: Object[] cannot be
converted to T[]
choiceArray = choices.toArray();
^
where T is a type-variable:
T extends Object declared in class Chooser
Object 배열을 T 배열로 형변환하면 된다.
choiceArray = (T[]) choices.toArray();
이번엔 경고가 뜬다.
Chooser.java:9: warning: [unchecked] unchecked cast
choiceArray = (T[]) choices.toArray();
^
required: T[], found: Object[]
where T is a type-variable:
T extends Object declared in class Chooser
T가 무슨 타입인지 알 수 없으니 컴파일러는 이 형변환이 런타임에도 안전한지 보장할 수 없다는 메시지다.
제네릭에서는 원소의 타입 정보가 소거되어 런타임에는 무슨 타입인지 알 수 없음을 기억하자!
위 코드는 동작하지만 컴파일러가 안전을 보장하지 못한다.
비검사 형변환 경고를 제거하려면 배열 대신 리스트를 쓰면 된다.
다음 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()));
}
}
'독서찰기(讀書札記) > 이펙티브 자바' 카테고리의 다른 글
[아이템 30] 이왕이면 제네릭 메서드로 만들라 (0) | 2022.02.12 |
---|---|
[아이템 29] 이왕이면 제네릭 타입으로 만들라 (0) | 2022.02.09 |
[아이템 27] 비검사 경고를 제거하라 (0) | 2022.02.07 |
[아이템 26] 로 타입(raw type)은 사용하지 말라 (0) | 2022.02.06 |
[아이템 25] 톱레벨 클래스는 한 파일에 하나만 담으라 (0) | 2022.02.05 |