31] 한정적 와일드 카드를 사용해 API 유연성을 높이라

매개변수화 타입은 불공변이다. 즉, 서로 다른 타입 Type1과 Type2가 있을 때 List<Type1>List<Type2>의 하위 타입도 상위 타입도 아니다. List<String>List<Object>의 하위 타입이 아니라는 뜻인데, 곰곰이 따져보면 사실 이쪽이 말이 된다. List<Object>에는 어떤 객체든 넣을 수 있지만 List<String>에는 문자열만 넣을 수 있다. 즉, List<String>List<Object>가 하는 일을 제대로 수행하지 못하니 하위 타입이 될 수 없다. 리스코프 치환 원칙에 어긋난다.

하지만 때론 불공변 방식보다 유연한 무언가가 필요하다. 여기 Stack의 public API를 추려보았다.

public class Stack<E> {
   public Stack();
   public void push(E e);
   public E pop();
   public boolean isEmpty();
}

이제 여기에 일련의 원소를 스택에 넣는 메서드( pushAll() )를 추가해야 한다고 해보자.

public void pushAll(Iterable<E> src) {
   for (E e: src)
      push(e);
}

이 메서드는 깨끗이 컴파일되지만 완벽하진 않다. Iterable src의 원소타입이 스택의 원소 타입과 일치하면 잘 작동한다. 하지만 Stack<Number>로 선언한 후 pushAll(intVal)을 호출하면 어떻게 될까? 여기서 intVal은 Integer 타입이다. Integer는 Number의 하위 타입이니 논리적으로는 잘 동작해야 할 것 같지만, 오류 메시지가 뜬다.

그 이유는 매개변수화 타입은 불공변이기 때문이다.

다행히 해결책은 있다. 한정적 와일드카드 타입이라는 타입을 지원한다. pushAll의 입력 매개변수 타입은 'E의 Iterable'이 아니라 'E의 하위 타입의 Iterable'이어야 하며, 와일드 카드 타입 Iterable<? extends E> 정확히 이런 뜻이다.

public void pushAll(Iterable<? extends E> src) {
   for (E e: src)
      push(e);
}

그 다음으로 짝을 이루는 popAll 메서드를 작성할 차례다. Stack 안의 모든 원소를 주어진 컬렉션으로 옮겨 담는 메서드다.

public void popAll(Collection<E> dst) {
   while (!isEmpty())
      dst.add(pop());
}

이번에도 역시나 완벽하진 않다. Stack<Number>의 원소를 Object용 컬렉션으로 옮기려 한다고 해보자.

Stack<Number> numberStack = new Stack<>();
Collection<Object> objects = ...;
numberStack.popAll(objects);