[Effective Java] 아이템 31 - 한정적 와일드카드를 사용해 API 유연성을 높이라
앞서 매개변수화 타입은 불공변이라 했는데 이는 List<Object>와 List<String>을 보면 직관적이다.
List<String>이 List<Object>의 역할을 수행하지 못하니 하위타입이 될 수 없다. 하위 타입은 LIP에 의해 상위타입의 역할을 할 수 있어야 되기 때문이다.
불공변 보다 유연한 무언가가 필요할 때가 있는데 대표적 예가 스택에 일련의 원소를 넣는 pushAll() 메서드 이다.
public void pushAll(Iterable<E> src) {
for(E e: src)
push(e);
}
위 코드는 src의 원소타입이 스택의 것과 동일하면 잘 돌아가지만 Stack<Number>로 선언후 pushAll(intVal)을 호출하면 타입 에러가 발생한다. 불공변(<Integer>가 <Number>의 하위타입이 아님)이기 때문에 당연하다.
그렇다면 Number의 하위 타입을 타입 매개변수로 받고 싶은 경우는 어떻게 해야할까
한정적 와일드카드 타입을 이용하면 된다. 'E의 Iterable'이 아닌 'E의 하위타입의 Iterable'을 받아야 하는 경우 Iterable<? extends E>가 정확한 표현이다. (자기 자신을 확장한 것은 아니기 때문에 extends 라는 표현이 어색하긴 하다) 이를 토대로 수정해 봤다
public void pushAll(Iterable<? extends E> src) {
for(E e: src)
push(e);
}
이제 pushAll 의 반대인 popAll을 작성해 보자. 스택의 모든 원소를 주어진 컬렉션에 옮겨 담는 메서드 이다.
public void popAll(Collection<E> dst) {
while (!isEmpty())
dst.add(pop());
}
//클라이언트 코드
Stack<Number> numberStack = new Stack<>();
Collection<Object> objects = new HashSet<>();
numberStack.popAll(objects);
이렇게 작성한다면 pushAll과 마찬가지로 타입 에러가 발생하게 된다. 이번에도 비슷한 맥락으로 'E의 Collection'이 아닌 'E의 상위 타입의 Collection'이 필요해서 생기는 문제이다. 이는 super를 이용해 해결한다
public void popAll(Collection<? super E> dst) {
while (!isEmpty())
dst.add(pop());
}
여기서 중요한 메세지가 나온다.
유연성을 극대화하려면 원소의 생성자나 소비자용 입력 매개 변수에 와일드 카드 타입을 사용하라
※단, 입력 매개변수가 생성자 이면서 소비자일 경우 타입을 정확히 지정해야 하므로 와일드 카드를 쓰지 말아야 한다.
한정적 와일드카드 타입을 만들때 super를 써야될지 extends를 써야할지 헷갈린다면 PECS를 기억해라
Producer - Extends, Consumer- Super
매개변수화 타입 T가 생산자이면 extends, 소비자라면 super를 이용해 만든다. pushAll의 src는 스택이 사용할 E인 인스턴스를 생성하므로 entends, popAll의 dts는 스택으로 부터 받은 E인 인스턴스를 사용하므로 super를 사용했다.
제대로만 사용한다면 클래스 사용자는 와일드카드 타입이 쓰였다는 사실조차 의식하지 못한다. 받아야할 타입만 받고 거절해야할 것은 알아서 걸러진다. 클래스 사용자가 와일드카드 타입을 신경써야 한다면 API에 문제가 있을 가능성이 크다
다음 max 메서드를 보자
public static <E extends Comparable<E>> E max(List<E> list)
// 와일드카트 타입을 이용해 개선한 모양이다
public static <E extends Comparable<? super E>> E max(List<? extends E> list)
입력 매개변수에서는 E 인스턴스를 생선 하므로 extends를 사용했다.
타입 매개변수가 문제다. 원래는 E가 Comparable<E>를 확장한다 라고 정의 했지만 Comparable<E>는 E 인스턴스를 소비하므로 super를 사용해 바꾸었다.
Comparable은 언제나 소비자이므로 대부분의 상황에서 Comparable<E>보단 Comparable<? super E>가 좋다.
이렇게 까지 복잡하게 만들 가치가 있다!
List<ScheduledFuture<?>> scheduledFutures = ...;
수정전 max 메서드는 ScheduledFuture가 Comparable<ScheduledFuture>를 구현하지 않았기 때문에 위 리스트를 처리할 수 없었다. ScheduledFuture는 Delayed의 하위 인터페이스이고 Delayed는 Comparable<Delayed>를 확장하는 구조로 되었이다. ScheduledFuture는 Delayed 까지 비교 가능해서 수정 전 max가 거부 했던 것이다.
즉, Comparable을 직접 구현하지 않고 직접 구현한 다른 타입을 확장한 타입을 지원하기위해 와일트카드가 필요하다.
마지막으로 타입 매개 변수와 와일드카드에 있어 둘다 괜찮은 경우가 많은데 이럴때는 어떤것을 쓸까?
공개 API라면 와일드 카드를 쓰는 편이 낫다.
public static <E> void swap(List<E> list, int i, int j);
public static void swap(List<?> list, int i, int j);
기본적으로는 메서드 선언에 타입 매개변수가 한번만 나오면 와일드 카드로 대체한다.
하지만 이렇게 하면 아래 swap에는 문제가 발생한다.
public static void swap(List<?> list, int i, int j){
list.set(i, list.set(j, list.get(i)));
}이렇게 직관적으로 구현한 코드가 컴파일되지 않는다. List<?>에는 null 외에는 들어가지 못하기 떄문이다. 이럴때는 와일드카드의 실제 타입을 알려주는 private 도우미 메서드를 만들어 활용한다.
public static void swap(List<?> list, int i, int j){
swapHelper(list, i, j);
}
private static <E> void swapHelper(List<E> list, int i, int j){
list.set(i, list.set(j, list.get(i)));
}
swapHelper는 리스트가 List<E>임을 알고있다. 즉 해당 리스트에서 꺼낸 값은 항상 E 이고 이 값은 항상 리스트에 넣어도 안전하다.
결론
조금 복잡하더라도 와일드카드 타입을 이용해 API를 작성하면 훨씬 유연해 지니까 적절히 사용해 주도록 하자.