[Effective Java] 아이템31 - 한정적 와일드카드를 사용해 API 유연성을 높이라
아이템28에서 살펴봤듯이 parametrized type은 invariant이다. 서로 다른 Type1, Type2가 있을 때, List
제너릭 사용 시, 이러한 invariant 성격 때문에 메소드 선언 시 하위타입의 원소를 같이 사용하지 못하는데, 이를 유연하게 사용할 수 있는 방법이 한정적 와일드카드 타입 (Bounded Wildcard Type) 이다.
와일드카드 타입을 사용하지 않은 pushAll 메서드
public void pushAll(Iterable<E> src) {
for(E e : src) {
push(e);
}
}
Stack<Number> numberStack = new Stack<>(); // Number 타입 스택 생성
Iterable<Integer> integers = ...;
numberStack.pushAll(integers);
// 에러! incompatible types : Iterable<Integer> cannot be converted to Iterable<Number>
Number는 Integer의 하위타입인데도 불구하고 Number 타입으로 선언된 Stack에 Integer 속성을 넣지 못한다. (vice versa도 마찬가지 일 것이다.)
와일드카드 타입을 사용한 pushAll 메서드
public void pushAll(Iterable<? extends E> src) {
for (E e : src) {
push(e);
}
}
<? extends E> : E의 하위 타입의 Iterable이어야 한다는 뜻이다.
pushAll 메서드는 input parameter인 src로부터 원소를 옮겨 담는 코드이다. 따라서 입력 파라미터를 producer라고 할 수 있다.
반대로, 메서드 안의 원소를 input paramter로 옮겨 담아서 전달한다면, 그 입력 파라미터는 consumer라고 부른다.
다음 consumer 역할을 하는 메서드를 보자.
와일드카드 타입을 사용하지 않은 popAll 메서드
public void popAll(Colllection<E> dst) {
while(!isEmpty()) {
dst.add(pop());
}
}
위와 같이, 원소를 꺼내서 입력 파라미터에 전달하는 경우, E가 스택의 원소 타입과 동일하면 문제가 되지 않는다.
하지만 아래와 같이, Stack
Stack<Number> numberStack = new Stack<>();
Collection<Object> objects = ...;
numberStack.popAll(objects);
Collection
와일드카드 타입을 사용한 popAll 메서드
public void popAll(Collection<? super E> dst) {
while(!isEmpty()) {
dst.add(pop());
}
}
Collection<? super E> : E의 상위타입의 Collection이어야 한다 라는 뜻이다. 따라서 앞에서 에러가 났던 부분이 해결이 된다.
이처럼 유연성을 극대화하려면 producer나 consumer input paramter에 wildcard type을 사용하자.
앞서 예제에서 봤듯이, producer은 extends를 썼고, consumer는 super을 사용했다. 이는 공식이다.
PECS : producer-extends, consumer-super
PECS 구체적 예
- union 메서드
public static <E> Set<E> union(Set<E> s1, Set<E> s2)
위와 같이 두 개의 Set을 받아서 하나의 Set으로 합치는 메서드가 있다고 가정할 때, s1 과 s2 모두 E의 producer이니 extends를 쓰면 된다.
public static <E> Set<E> union(Set<? extends E> s1, Set<? extends E> s2)
return type은 여전히 Set<E> 인 것에 주목하자. 반환 타입에는 한정적 와일드카드 타입을 사용하면 안된다.
- 위 코드는 java8 부터 제대로 컴파일 된다. java7 까지는 타입 추론 능력이 강력하지 못해서 타입을 명시해야 한다.
// Union.class의 union 메서드
Set<Number> numbers = Union.<Number> union(integers, doubles);
- 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)
- List
list 에서 List<? extends E> 로 변경했다. E 인스턴스를 생산해서 사용하기 떄문이다. - Comparable
가 E 인스턴스를 소비해서 선후 관계를 뜻하는 정수를 생산하기 때문에 super 와일드카드 타입을 사용했다. Comparable,Comprator은 언제나 consumer 임을 기억하자.
이렇게 복잡하더라도 와일드카이드 타입은 사용할만한 가치가 있다. 왜냐하면, 상위클래스/하위클래스 사이에 복잡한 관계가 있을 수 있기 때문이다.
다음 리스트는 수정된 max로만 처리할 수 있다.
List<ScheduledFuture<?>> scheduledFutures = ...;
ScheduledFuture이 Comparable<ScheduledFuture>를 구현하지 않았기 때문에 수정된 max 메서드에서는 실행이 불가하다.
아래 사진과 같이, ScheduledFuture은 Delayed의 하위 인터페이스인데, 이 Delayed는 Comprable
다시말해, ScheduledFuture이 다른 Delayed 인스턴스를 확장한 클래스와 비교할 가능성이 있기 때문에 Type Safety 하지 않아서 수정전 max가 이를 거부하는 것이다.

타입 매개변수(Parametrized Type)과 와일드카드(Wild Type)
<T>나 <E> 처럼 타입 매개변수를 받는 것과 <?> 처럼 와일드카드 타입을 사용하는 것은 어떻게 보면ㅌ 비슷해보인다.
만약 메서드 선언에 타입 매개변수가 한 번만 나오면 와일드카드로 대체하자.
public static <E> void swap(List<E> list, int i, int j);
public static void swap(List<?> list, int i, int j); // 한번밖에 안나왔으니, 이렇게 쓰자
private helper 메서드
하지만 아래와 같은 두번째 swap 메소드는 실제로 컴파일이 되지 않는다.
public static void swap(List<?> list, int i, int j) {
list.set(i, list.set(j, list.get(i)));
}
list.get(i)를 가져와서 list에 다시 set 하는 부분에서 에러가 난다.

왜냐하면 비한정적 와일드타입인 <?>은 말그대로 Unknown Type이므로, list.get(i) 을 해온 타입이 <?> 알수없는 타입의 하위타입인지 알 방법이 없기 때문이다.
따라서, 와일드카드 타입의 실제 타입을 알려주는 메서드를 private 도우미 메서드로 따로 작성해서 활용한다.
public static void swap(List<?> list, int i, int j) {
swapHelper(list, i, j);
}
// 와일드카드 타입을 실제 타입으로 바꿔주는 private 도우미 메서드
private static <E> void swapHelper(List<E> list, int i, int j) {
list.set(i, list.set(j, list.get(i)));
}
swapHelper 메서드는 제네릭 메서드로, 컴파일 시점에는 이 리스트에서 꺼낸 값은 E이고, set할때도 E임을 알고 있다. 또한 private으로 선언해서 클라이언트에서 이러한 도우미 메서드가 있는지 알지 못하게 할 수 있는 장점이 있다.
댓글남기기