List<Type1>은 List<Type2>의 하위 타입도 상위 타입도 아니다. List<String>은 List<Object>의 하위 타입이 아니라는 말과 같다. List<String>은 문자열만 넣을 수 있기 때문에 List<Objec>가 하는 일을 제대로 수행하지 못하기 때문이다. (리스코프 치환 원칙)
Number 는 Integer의 상위 타입이다. 하지만 제네릭으로 정의했을 때는 상위타입도 아니고, 하위타입도 아니다. 때문에 제네릭을 사용하면 유연한 코드를 작성하지 못할 수 있다. 그래서 우리는 불공변 방식보다 유연한 무언가가 필요하다.
자바는 위 예시 코드와 같은 문제를 해결하기 위해 한정적 와일드카드 타입이라는 특별한 매개변수화 타입을 지원한다. Iterable<? extends E>
는 E의 하위 타입인 Iterable 이라는 뜻이다.
위 예시 코드에서 Stack 안의 모든 원소를 주어진 컬렉션으로 옮겨 담으려면 어떻게 해야 할까. 같은 제네릭 타입으로는 옮겨 담을 수 있겠지만, 상위 타입으로 정의된 제네릭 타입으로는 옮겨 담을 수 없다. 이런 경우에도 한정적 와일드카드 타입을 사용하여 해결할 수 있다. Iterable<? super E>
는 E의 상위 타입인 Iterable 이라는 뜻이다.
유연성을 극대화하려면 원소의 생산자나 소비자용 입력 매개변수에 와일드카드 타입을 사용하라. 한편, 매개변수가 생산자와 소비자 역할을 동시에 한다면 와일드카드 타입을 써도 좋을게 없다. 이럴 대는 와일드카드 타입을 쓰지 말아야 한다.
아래 코드에서 pushAll 메서드의 파라미터는 생산자이니까 <? extends E>
를 사용해야 하고 popAll 메서드의 파라미터는 소비자이니까 <? super E>
를 사용해야 하는 것이다.
static class Stack<E> {
final List<E> list = new LinkedList<>();
void push(E e) {
list.add(e);
}
void pushAll(Iterable<? extends E> src) {
for (E e : src) {
push(e);
}
}
E pop() {
if (list.isEmpty()) {
throw new EmptyStackException();
}
int lastIndex = list.size() - 1;
E e = list.get(lastIndex);
list.remove(lastIndex);
return e;
}
void popAll(Collection<? super E> dst) {
while (!list.isEmpty()) {
dst.add(pop());
}
}
}