💥 개요
배열과 제네릭 타입에는 중요한 차이가 두가지 있다.
배열의 경우 Sub가 Super의 하위 타입이라면 배열 Sub[]는 배열 Superp[]의 하위 타입이 된다.(공변)
하지만 제네릭은 불공변이다. List<Sub>가 List<Super>의 하위 타입이 아니다.(불공변)
그럼 제네릭이 더 안 좋은걸까?
배열의 문제점
Object[] objectArray = new Long[1];
objectArray[0] = "타입이 달라 넣을 수 없다."; // ArrayStoreException을 던진다.
위의 코드는 런타임에 실패한다.
하지만 다음 코드는 컴파일 시점에 실패한다.
List<Object> ol = new ArrayList<Long>(); // 호환되지 않는 타입이다.
ol.add("타입이 달라 넣을 수 없다.");
어느쪽이든 문자열을 넣을 수 없지만, 이처럼 컴파일 시점에 알지못해 위험성을 가지게 된다.
배열은 실체화된다(reify)
위 코드에서 배열은 Long 배열에 String을 넣으려 하면 ArrayStoreException이 발생한다.
하지만 제네릭은 타입 정보가 런타임에는 소거(erasure)된다. 원소 타입을 컴파일 타임에만 검사하며, 런타임에는 알 수조차 없게 된다는 뜻이다.
소거 : 제네릭이 지원되기 전의 레거시 코드와 제네릭 타입을 함께 사용할 수 있게 해주는 메커니즘
둘은 어울릴 수 없다
이 차이로 둘은 잘 어우러지지 못한다. 배열은 제네릭 타입, 매개변수화 타입, 타입 매개변수로 사용할 수 없다.
그 이유는 타입 안전하지 않기 때문이다. 이를 허용하면 컴파일러가 자동 생성한 형변환 코드에서 런타임에 ClassCastException이 발생할 수 있다. 런타임에 ClassCastException이 발생하는 일을 막아주는 취지에 어긋난다.
List<String>[] stringLists = new List<String>[1];
List<Intger> intList = List.of(42);
Obejct[] objects = stringLists;
objects[0] = intList;
String s = stringLists[0].get(0);
line1이 허용된다고 가정하면 2는 원소가 하나인 리스트를 생성한다.
3은 1에서 생성한 List<String> 배열을 Obejct 배열에 할당한다.(배열은 공변이니 아무 문제 없다.)
4는 2에서 생성한 List<Integer>의 인스턴스를 Obejct배열의 첫 원소로 저장한다.(제네릭은 소거 방식이라 문제없다)
즉, 런타임에는 List<Integer> 인스턴스 타입은 단순히 List가 되고, List<Integer>[] 인스턴스 타입은 List[]가 된다.
따라서 4에도 ArrayStoreException이 일어나지 않는다.
하지만 List<String>만 담겠다고 선언한 배열에는 지금 List<Integer>가 담겨있다.
5에서 리스트의 첫 원소를 꺼내려 할 때, 컴파일러는 String으로 형변환을 시도하는데 Integer이므로 ClassCastException이 발생하게 되는 것 이다.
실체화 불가 타입
E, List<E>, List<String>과 같은 타입을 실체화 불가 타입이라 한다.
런타임에는 컴파일타임보다 타입 정보를 적게 가지는 타입이다.
소거 매커니즘으로 인해 실체화 될 수 있는 타입은 List<?>와 Map<?,?> 같은 비한정적 와일드카드 타입 뿐이다.
배열을 제네릭으로 만들 수 없어 귀찮을 때도 있다.
제네릭 컬렉션에서는 자신의 원소 타입을 담은 배열을 반환하는게 보통은 불가능하다.
또한 제네릭과 가변 인수 메서드를 쓰면 해석하기 어려운 경고 메시지를 받기도 한다. 이 문제는 @SafeVarargs로 대처가 가능하다.
public class Chooser {
private final Object[] choiceArray;
public Chooser(Collection<T> choices) {
choiceArray = choises.toArray();
}
public Object choose() {
Random rnd = ThreadLocalRandom.current();
return choiceArray[rnd.nectint(choiceArray.length)];
}
}
배열로 형변환 할 때 제네릭 배열 생성 오류나 비검사 형변환 경고가 뜨는 경우 대부분은 배열인 E[] 대신 컬렉션 List<E>를 사용하면 해결된다. 코드가 조금 복잡해지고, 성능이 살짝 나빠질 수 있지만 더 안전하고 상호 운용성은 좋아진다.
위의 Chooser는 매번 형 변환이 필요하고 문제가 생길 수 있어서 제네릭을 적용해야 한다.
public class Chooser<T> {
private final List<T> choiceList;
public Chooser(Collection choices) {
choiceList = new ArrayList<>(choises);
}
public Object choose() {
Random rnd = ThreadLocalRandom.current();
return choiceList.get(rnd.nextint(choiceList.size()));
}
}
이전보다 코드 양이 늘고, 아마 조금 더 느리지만 런타임에 에러를 만날 일은 없으니 그만한 가치가 있다.
제네릭은 불공변이고 타입 정보가 소거된다. 그 결과 배열은 런타임에는 타입 안전하지만 컴파일 타임에는 그렇지 않다. 제네릭은 반대다. 그래서 둘을 섞어 쓰기란 쉽지 않다. 둘을 섞어 쓰다가 컴파일 오류나 경고를 만나면, 가장 먼저 배열을 리스트로 대체하는 방법을 적용해보자.
'Study > 이펙티브 자바' 카테고리의 다른 글
[Effective Java 3E] 한정적 와일드카드를 사용해 API 유연성을 높이라 (0) | 2023.12.02 |
---|---|
[Effective Java 3E] 이왕이면 제네릭 타입으로 만들라 (1) | 2023.11.28 |
[Effective Java 3E] 비검사 경고를 제거하라 (1) | 2023.11.09 |
[Effective Java 3E] 로 타입은 사용하지 말라 (0) | 2023.11.01 |