티스토리 뷰
배열과 제네릭의 차이
1) 배열은 공변(함께 변한다)이다. → Sub가 Super의 하위타입이면 Sub[]는 Super[]의 하위타입이 된다.
제네릭은 불공변이다. → 서로 다른 Type1, Type2 가 있으면 List은 List의 하위타입도 상위타입도 아니다.
//배열 : 런타임 오류 -> 문제가 있다!
Object[]objectArray = new Long[1];
objectArray[0] = "타입이 달라 넣을 수 없다."; // ArrayStoreException을 던진다.
//제네릭 : 컴파일 오류
List<Object> ol = new ArrayList<Long>(); // 호환되지 않는 타입이다.
ol.add("타입이 달라 넣을 수 없다.");
2) 배열은 실체화된다.
- 배열은 런타임에도 자신이 담기로 한 원소의 타입을 인지하고 확인함
- 반면, 제네릭은 타입 정보가 런타임에는 소거됨 → 컴파일 타임에만 검사, 소거는 제네릭 지원 전의 코드와 어우러지도록 하기 위한 메커니즘
배열과 제네릭의 부조화
- 배열은 제네릭 타입(new List[]), 매개변수화 타입(new List[]), 타입 매개변수(new E[])로 사용할 수 없다. → 제네릭 배열 생성 오류
- 제네릭 배열을 만들지 못하게 한 이유 : 타입 안전하지 않음 (컴파일러가 자동 생성한 형변환 코드에서 런타임에 ClassCastException 이 발생할 수 있다. 런타임에 이 예외가 발생하지 않도록 하는 것이 제네릭 타입 취지기때문에 모순임)
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)과 같은 제네릭 배열 생성이 가능하다면?
(2)에서는 원소가 하나인 리스트를 생성하고, (3)에서는 배열은 공변이므로 할당이 된다.
(4)에서는 제네릭은 소거방식이므로 런타임 시에 List는 List가 되고, List[]는 List[]가 된다. 그러므로 ArrayStoreException이 발생하지 않고 지나간다.
(5)에서가 문제다. 현재 stringLists의 0번째에는 List 인스턴스가 들어가있다.
꺼낸 원소를 Integer → String 으로 형변환을 하지 못해서 런타임에 ClassCastException 이 발생한다.
이런 런타임 시에 예외 발생을 막기 위해 제네릭 배열 생성을 막아서 컴파일 오류를 내야함
실체화 불가 타입(non-reifiable type)
- E, List, List 같은 타입 → 런타임에 컴파일 타임보다 타입 정보를 적게 가짐.
- 소거 매커니즘 때문에 List<?> 나 Map 같은 비한정적 와일드카드 타입만 실체화 가능
- 제네릭 타입과 가변인수 메서드(varargs method)를 함께 쓰면 경고를 받게 된다. (실체화 불가 타입이므로)
→ @SafeVarargs 어노테이션으로 대처 가능 - 배열로 형변환할때 제네릭 배열 생성오류나 비검사 형변환 경고가 뜨면 컬렉션인 List를 사용하자.
public class Chooser {
private final Object[] choiceArray;
public Chooser(Collection choices) {
choiceArray = choices.toArray();
}
public Object choose() { //메서드 호출 시 반환된 Object를 원하는 타입으로 형변환 필요
Random rnd = ThreadLocalRandom.current();
return choiceArray[rnd.nextInt(choiceArray.length)];
}
}
위 코드의 choose() 메서드를 호출할때마다 반환된 Object를 원하는 타입으로 형변환해야하고, 만약 타입이 다른 원소가 들어있었다면 런타임에 형변환 오류가 난다. → 제네릭 타입으로 변환
public class Chooser<T> {
private final T[] choiceArray;
public Chooser(Collection<T> choices) {
//toArray()를 Object[] 타입으로 형변환 할수없어서 컴파일 오류
choiceArray = choices.toArray();
}
}
public class Chooser<T> {
private final T[] choiceArray;
public Chooser(Collection<T> choices) {
choiceArray = (T[]) choices.toArray(); //경고
}
}
위 코드는 T가 무슨 타입인지 알 수 없어서 형변환이 런타임에도 안전하다고 보장할 수 없다고 경고가 뜬다.
제네릭은 런타임에 타입 정보가 소거되어 타입을 알 수 없어서 안전하진 못하다.
이 경고를 제거하기 위해서는 배열 대신 리스트를 쓰면 된다.
//배열 대신 리스트 사용 : 런타임 시 ClassCastException을 피한다.
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));
}
}
결론
배열은 공변이고 실체화되지만, 제네릭은 불공변이고 타입 정보가 소거된다.
그래서 배열은 런타임에 타입 안전하지만, 컴파일 타임에는 그렇지 않다.
제네릭은 반대로 런타임에는 안전하지 않고, 컴파일 타임에는 안전하다.
그래서 둘을 섞어쓰기는 쉽지 않다 → 배열을 리스트로 대체하자.
'Java > Effective Java' 카테고리의 다른 글
[Effective Java] 30.이왕이면 제네릭 메서드로 만들라 (0) | 2022.03.23 |
---|---|
[Effective Java] 29.이왕이면 제네릭 타입으로 만들라 (0) | 2022.03.23 |
[Effective Java] 27.비검사 경고를 제거하라 (0) | 2022.03.18 |
[Effective Java] 26.로 타입은 사용하지 말라 (0) | 2022.03.18 |
[Effective Java] 25.톱레벨 클래스는 한 파일에 하나만 담으라 (0) | 2022.03.17 |