'이펙티브 자바'를 읽으며 작성하는 글
매개변수화 타입은 불공변(invariant)이다. 즉, 서로 다른 타입 Type1와 Type2가 있을 때 List<Type1>과 List<Type2>는 서로 하위 타입도 상위 타입도 아니다. List<String>은 List<Object> 의 하위 타입이 아니란 뜻인데, 의아할 수 있지만 이것이 더 말이 된다. List<Object>에는 어떠한 객체든 들어갈 수 있지만, List<String>에는 문자열만 들어갈 수 있으므로 List<String>은 List<Object>가 하는 일을 제대로 수행하지 못 한다. 이는 리스코프 치환 원칙에 어긋난다. 하지만 때론 불공변 방식보다 유연한 무언가가 필요하다. Stack 클래스를 통해 확인해보자. 다음은 Stack의 public API를 추려본 것이다.
public class Stack<E> {
public Stack();
public void push(E e);
public E pop();
public boolean isEmpty();
여기에 일련의 원소를 스택에 넣는 메서드를 추가해야 한다고 가정한다.
이 메서드는 문제 없이 컴파일되지만 완벽하진 않다. Iterable src의 원소 타입이 스택의 원소 타입과 일치하면 잘 작동한다. 하지만 Stack<Number>로 선언한 후 pushAll(intVal)을 호출하면(intVal은 Integer 타입) Integer는 Number의 하위 타입이니 논리적으론 잘 작동하여야 맞는 것 같지만 실제로는 매개변수화 타입이 불공변이기 때문에 에러가 발생한다.
이러한 상황에서의 해결책은 한정적 와일드카드를 사용하는 것이다. pushAll의 입력 매개변수의 타입은 E의 Iterable이 아니라 E의 하위타입의 Iterable이어야 한다. 와일드카드 Iterable<? extends E>가 정확히 이런 뜻이다. 다음은 pushAll 메서드를 한정적 와일드카드 타입을 사용하도록 수정한 것이다.
이제 pushAll 메서드와 짝을 이룰 popAll 메서드를 작성하고다 한다. popAll 메서드는 Stack 안의 모든 원소를 주어진 컬렉션으로 옮겨 담는 역할을 한다. 다음과 같이 작성한다고 가정한다.
이번에도 주어진 컬렉션의 원소 타입이 스택의 원소 타입과 일치한다면 말끔히 컴파일되고 문제없이 동작한다. 하지만 이번에도 역시나 완벽하진 않다. Stack<Number>의 원소를 Object용 컬렉션으로 옮긴다고 가정하면 에러가 발생하게 된다. 이번에는 popAll 메서드의 입력 매개변수 타입이 E의 Collection이 아니라 E의 상위 타입의 Collection이어야 한다. 이를 popAll 메서드에 적용한 모습이다.
이제 Stack과 클라이언트 코드 모두 말끔히 컴파일된다.
유연성을 극대화하려면 원소의 생산자나 소비자용 입력 매개변수에 와일드카드 타입을 사용하라.