Item 28 - 배열보다는 리스트를 사용하라
배열과 제네릭의 차이
1) 배열은 공변, 제네릭은 불공변이다.
- 배열 : Sub가 Super의 하위타입이면 배열
Sub[]
는 배열Super[]
의 하위타입이다. - 제네릭 : 서로 다른
T1
,T2
가 있을 때,List<T1>
는List<T2>
의 하위타입도 상위타입도 아니다.
1
2
3
// 배열 : 런타임에 실패한다.
Object[] objectArray = new Long[1];
objectArray[0] = "타입이 달라 넣을 수 없다." // ArrayStoreException을 던진다.
1
2
3
// 제네릭 : 컴파일되지 않는다.
List<Object> o1 = new ArrayList<Long>(); // 호환되지 않는 타입이다.
o1.add("타입이 달라 넣을 수 없다.");
2) 배열은 실체화된다.
- 배열은 런타임에도 자신이 담기로 한 원소의 타입을 인지하고 확인한다.
- 반면, 제네릭은 타입 정보가 런타임에는 소거된다.
- 컴파일 타임에만 원소 타입을 검사한다.
- 소거는 제네릭 지원 전 레거시 코드와 제네릭 타입을 함께 사용할 수 있게 해주는 메커니즘
배열과 제네릭의 부조화
- 배열은 제네릭 타입, 매개변수화 타입, 타입 매개변수로 사용할 수 없다.
new List<E>[]
,new List<String>[]
,new E[]
- 제네릭 배열 생성 오류
- 제네릭 배열을 만들지 못하게 막은 이유
- 타입 안전하지 않기 때문.
- 컴파일러가 자동 생성한 형변환 코드에서 런타임에
ClassCastException
이 발생할 수 있다.- 런타임에 해당 예외가 발생하지 않도록 하는 것이 제네릭 타입의 취지이므로 모순이다.
(1)과 같은 제네릭 배열 생성이 가능하다면?
1
2
3
4
5
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)
- (2)는 원소가 하나인
List<Integer>
를 생성한다. - (3)은 (1)에서 생성한
List<String>
의 배열을Object
배열에 할당한다 - (4)는 (2)에서 생성한
List<Integer>
의 인스턴스를Object
배열의 첫 원소로 저장한다.- 제네릭은 소거방식으로 구현되어서 런타임시에
List<Integer>
의 인스턴스 타입은 단순히List
가 되고,List<Integer>[]
의 인스턴스 타입은List[]
가 된다. - 따라서
ArrayStoreException
이 발생하지 않는다.
- 제네릭은 소거방식으로 구현되어서 런타임시에
- (5)에서가 문제다.
- 현재 stringLists의 0번째에는
List<Integer>
인스턴스가 저장돼 있다. - 컴파일러는 꺼낸 원소를 자동으로
String
으로 형변환 하는데, 이 원소가Integer
이므로 런타임에ClassCastException
오류가 발생한다. - 이런 일을 방지하려면 제네릭 배열이 생성되지 않도록 (1)에서 컴파일 오류를 내야 한다.
- 현재 stringLists의 0번째에는
실체화 불가 타입(non-reifiable type)
E
,List<E>
,List<String>
같은 타입- 런타임에 컴파일 타임보다 타입 정보를 적게 가진다.
- 소거 매커니즘 때문에
List<?>
나Map<?, ?>
같은 비한정적 와일드카드 타입만 실체화 가능하다. - 제네릭 타입과 가변인수 메서드(vargargs method)를 함께 쓰면 경고를 받게 된다.
- 실체화 불가 타입이기 때문이다.
@SafeVarargs
어노테이션으로 대처 가능하다.
- 배열로 형변환 시, 제네릭 배열 생성 오류나 비검사 형변환 경고가 뜨면 배열인
E[]
대신 컬렉션인List<E>
를 사용하자.
1
2
3
4
5
6
7
8
9
10
11
12
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
를 원하는 타입으로 형변환해야한다.- 혹시나 타입이 다른 원소가 들어있었다면 런타임에 형변환 오류가 발생한다.
이 클래스를 제네릭 타입으로 변환해보자.
1
2
3
4
5
6
7
8
9
// Chooser를 제네릭으로 만들기 위한 첫 시도 - 컴파일 되지 않는다.
public class Chooser<T> {
private final T[] choiceArray;
public Chooser(Collection<T> choices) {
// toArray()를 Object[] 타입으로 형변환 할 수 없어서 컴파일 오류
choiceArray = choices.toArray();
}
}
toArray()
를Object[]
타입으로 형변환 할 수 없어서 컴파일 오류가 발생한다.
1
2
3
4
5
6
7
8
// Object 배열을 T 배열로 형변환 하자.
public class Chooser<T> {
private final T[] choiceArray;
public Chooser(Collection<T> choices) {
choiceArray = (T[]) choices.toArray(); // 경고
}
}
- 제네릭은 런타임에 타입정보가 소거되어 런타임에는 타입을 알 수 없으므로 안전하지 않다.
비검사 형변환 경고를 제거하기 위해서는 배열 대신 리스트를 쓰면 된다.
1
2
3
4
5
6
7
8
9
10
11
12
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));
}
}
💡 핵심 정리
- 배열은 공변이고 실체화되지만, 제네릭은 불공변이고 타입 정보가 소거 된다.
- 그래서 배열은 런타임에 타입 안전하지만, 컴파일 타임에는 그렇지 않다.
- 제네릭은 반대로 런타임에는 안전하지 않고, 컴파일 타임에는 안전하다.
- 그래서 둘을 섞어 쓰기는 쉽지 않다. → 배열을 리스트로 대체하자.
참고 🕶️
@SafeVarags
- SafeVarargs (Java Platform SE 7)
- 생성자와 메서드의 제네릭 가변인자에 사용할 수 있는 애노테이션
- 제네릭 가변인자는 근본적으로 타입 안전하지 않다.
- 가변인자가 배열이므로, 제네릭 배열과 같은 문제가 발생한다.
- 가변 인자 (배열)의 내부 데이터가 오염될 가능성이 있다.
@SafeVarargs
를 사용하면 가변 인자에 대한 해당 오염에 대한 경고를 숨길 수있다.- 아이템 32. 제네릭과 가변인수를 함께 쓸 때는 신중하라
This post is licensed under CC BY 4.0 by the author.