배열과 제네릭 타입의 차이
1. 배열은 공변(covariant)이다. (함께 변한다는 뜻)
예를들어 Sub가 Super의 하위 타입이라면 배열 Sub[]는 배열 Super[]의 하위 타입이 된다.
반면에 제네릭은 불공변(invariant)이다. List<Sub>는 List<Super>의 하위 타입도 아니고 상위 타입도 되지 않는다.
아래 코드처럼 배열에서는 코드가 실행되는 런타임 시점에서야 오류가 발생함을 알 수 있지만 리스트의 경우 컴파일 시점에 오류를 확인할 수 있다.
Object[] objectArray = new Long[1];
// ArrayStoreException 발생
objectArray[0] = "Kimtaeng";
// 아예 컴파일 오류
List<Object> objectList = new ArrayList<Long>();
objectList.add("Kimtaeng");
2. 배열은 실체화(reify) 된다.
런타임에도 자신이 담기로 한 원소의 타입을 확인함을 뜻한다. 위의 코드에서 ArrayStoreException이 발생한 것도 같은 이유.
하지만 제네릭은 타입 정보가 런타임 시점에 소거된다. 원소 타입을 컴파일 시점에만 검사하기 때문에 런타임 시점에는 알 수 없다.
왜 제네릭 배열을 생성하지 못할까?
배열은 아래와 같이 제네릭 타입, 매개변수화 타입, 타입 매개변수로 사용할 수 없다.
// 배열은 아래와 같이 사용하면 오류가 발생한다.
new List<E>[]; // 제네릭 타입
new List<String>[]; // 매개변수화 타입
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)
- (3)번: (1)번 과정에서 생성된다고 가정한 제네릭 배열을 Object[]에 할당. 배열은 공변이므로 아무런 문제가 없다.
- (4)번: (2)에서 생성한 List<Integer>의 인스턴스를 Object 배열의 첫 원소로 저장한다. 제네릭은 런타임 시점에서 타입이 사라지므로 List<Integer>은 List가 되고 List<String>[]는 List[]가 된다. 따라서 ArrayStoreException이 발생하지 않는다.
- *(5)번: List<String> 인스턴스만 담겠다고 선언한 stringLists 배열에 다른 타입의 인스턴스가 담겨있는데 첫 원소를 꺼내려 한다. 그리고 이를 String으로 형변환하는데, 이 원소는 Integer 타입이므로 런타임에 ClassCastException이 발생한다.
애초에 제네릭 배열이 생성되지 않도록 (1)에서 컴파일 오류가 발생해야한다!!
배열로 형변환시 오류가 발생한다면
배열로 형변환할 때 제네릭 배열 생성 오류나 비검사 형변환 경고가 뜨는 경우 대부분은 배열인 E[] 대신에 컬렉션인 List<E>를 사용하면 해결된다.
choose메서드(컬렉션 안 원소 중 하나를 무작위로 선택해 반환)를 구현해보자.
[제네릭을 쓰지 않고 구현한 버전]
public class Chooser {
private final Object[] choiceArray;
public Chooser(Collection choices) { // 생성자에서 Collection을 받는 클래스
this.choiceArray = choices.toArray();
}
// 이 메서드를 사용하는 곳에서는 매번 형변환이 필요하다.
// 형변환 오류의 가능성이 있다.
public Object choose() {
Random rnd = ThreadLocalRandom.current();
return choiceArray[rnd.nextInt(choiceArray.length)];
}
}
타입이 다른 원소가 들어가 있다면 형변환 오류가 발생한다.
[E[]로 구현] - 컴파일 되지 않음
public class Chooser<T> {
private final T[] choiceArray;
public Chooser(Collection<T> choices) {
// 오류 발생 incompatible types: java.lang.Object[] cannot be converted to T[]
this.choiceArray = choices.toArray();
}
// choose 메소드는 동일하다.
}
// Object 배열을 T 배열로 형변환하면 된다.
this.choiceArray = (T[]) choices.toArray();
컴파일 오류는 사라졌지만 Unchecked Cast 경고가 발생한다. (타입 매개변수 T가 어떤 타입인지 알 수 없으니 형변환이 런타임에도 안전한지 보장할 수가 없다는 메시지)
제네릭은 런타임에는 타입 정보가 소거되므로 무슨 타입인지 알 수 없다. Unchecked Cast과 같은 비검사 형변환 경고를 제거하려면 배열 대신 리스트를 사용하면 된다.
class Chooser<T> {
private final List<T> choiceList;
public Chooser(Collection<T> choices) {
this.choiceList = new ArrayList<>(choices);
}
public T choose() {
Random rnd = ThreadLocalRandom.current();
return choiceList.get(rnd.nextInt(choiceList.size()));
}
}
'Java' 카테고리의 다른 글
[Effective Java] 아이템30. 이왕이면 제네릭 메서드로 만들라 (0) | 2022.05.22 |
---|---|
[Effective Java] 아이템29. 이왕이면 제네릭 타입으로 만들라 (0) | 2022.05.22 |
[Effective Java] 아이템 27. 비검사 경고(unchecked warning)를 제거하라 (0) | 2022.05.08 |
[Effective Java] 아이템 26. Raw타입은 사용하지 말라. (0) | 2022.05.08 |
[Effective Java] 아이템20. 추상 클래스보다는 인터페이스를 우선하라 (0) | 2022.04.25 |