배열은 공변(자신이 상속받은 부모 클래스로 타입을 변환시키기 가능)
Object[] objectArray = new Long[1];
objectArray[0] = "타입이 달라 넣을 수 없음"; //ArrayStoreException을 던진다.
이 코드는 뭄법상 가능한데 런타임 에러가 발생한다
하지만 제네릭은 불공변이다
List<Object> o1 = new ArrayList<Long>(); // 호환되지 않는 타입
o1.add("저장 안되는 문자열");
이거는 컴파일 에러가 발생한다 배열에서는 이러한 실수를 런타임에서 알지만 리스트를 사용하면 컴파일할때 알 수 있다
또한 배열을 실체화가 된다
배열은 런타임에도 자신의 원소 타입을 인지하고 확인한다. 하지만 제네릭은 런타임에는 소거된다. 컴파일타임에 검사하며 런타임에는 알 수 없는 것이다. 소거는 제네릭이 지원되기 전의 레거시 코드와 제네릭 타입을 함께 사용할 수 있게 해주는 메커니즘을 위한 것이다 이런 차이로 제네릭과 배열은 잘 어우러지지 못한다 배열은 제네릭 타입, 매개변수화 타입, 타입 매개변수로 사용할 수 없다. 즉, 코드를 new List[], new List[], new E[]식으로 작성하면 컴파일할 때 제네릭 배열 생성 오류를 일으킨다
그럼 제네릭 배열을 왜 막아놨을까? 타입 안전하지 않기 때문이다. 이를 허용한다면 컴파일러가 자동 생성한 형변환 코드에서 런타임에 ClassCaseException이 발생할 수 있다. 런타임에 ClassCaseException이 발생하는 일을 막아주겠다는 제네릭 타입 시스템의 취지에 어긋나는 것이다
List<String>[] stringLists = new List<String>[1]; // (1)
List<Integer> intList = List.of(42); // (2)
Object[] objects = stringList; // (3)
objects[0] = intList; // (4)
String s = stringLists[0].get(0); // (5)
1이 허용한다고 가정해보자. 2는 원소가 하나인 List를 생성한다 3은 1에서 생성한 List의 배열을 Object 배열에 할당한다. 배열은 공변이니 ㄱㅊ. 4는 2에서 생선한 List 인스턴스를 Object 배열의 첫 원소로 저장한다. 제네릭은 소거 방식으로 구현되어서 이 역시 성공한다 즉 런타임에는 List 인스턴스 타입은 단순히 List가 되고, List[] 인스턴스 타입은 List[]가 된다. 따라서 4도 ArrayStoreException을 일으키지 않는다 하지만 5에서 List 인스턴스만 담겠다고 선언한 stringLists에 List 인스턴스가 저장되어있다. 그 원소를 String으로 형변환하는데 이 원소는 Integer이므로 런타임에 ClassCastException이 발생한다
E, List, List 같은 타입을 실체화 불가 타입(non-relifiable type)이라 한다. 쉽게 말해 실체화되지 않아서 런타임에는 컴파일 타임보다 타입 정보를 적게 가지는 타입이다
제네릭 컬렉션에서 자신의 원소타입을 담은 배열을 반환하는게 불가능하다(예외 아이템 33)
배열로 형변환할 때 제네릭 배열 생성 오류나 형변환 경고가 뜨는 경우 E[]대신 컬렉션 List를 사용하면 해결된다. 코드가 복잡해지고 성능이 살짝 나빠질 수 있지만, 타입 안정성과 상호운용성은 좋아진다.
ex)
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를 원하는 타입으로 형 변환 해야한다 혹시 다른 타입의 원소가 들어있으면 런타임에 오류가 난다 이걸 제네릭으로 만들어보자
public class Chooser<T> {
private final T[] choiceArray;
public Chooser(Collection<T> choices) {
choiceArray = choice.toArray();
}
}
이렇게 하면 컴파일되지 않는다 그러면 Object 배열을 T배열로 캐스팅 하면 된다
choiceArray = (T[]) choices.toArray(); 이러면 경고가 뜬다
public class Chooser<T> {
private final T[] choiceArray;
public Chooser(Collection<T> choices) {
choiceArray = (T[]) choices.toArray();
}
}
T가 무슨타입인지 알 수 없으니까 런타임에서 안전함을 보장할 수 없다는 경고이다 그러면 이제 배열대신 리스트를 쓰면 된다
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()));
}
}
코드양이 늘고 느릴지랃 런타임에 ClassCastException을 만날일이 없다