[Effective Java] 아이템31 - 한정적 와일드카드를 사용해 API 유연성을 높이라

아이템28에서 살펴봤듯이 parametrized type은 invariant이다. 서로 다른 Type1, Type2가 있을 때, List은 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>

NumberInteger의 하위타입인데도 불구하고 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에 Collection타입의 입력 매개변수로 popAll을 하면 에러가 난다.

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

Collection는 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 구체적 예

  1. 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);

깃코드-Union

  1. 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 = ...;

ScheduledFutureComparable<ScheduledFuture>를 구현하지 않았기 때문에 수정된 max 메서드에서는 실행이 불가하다.

아래 사진과 같이, ScheduledFuture은 Delayed의 하위 인터페이스인데, 이 Delayed는 Comprable를 구현했다.

다시말해, ScheduledFuture이 다른 Delayed 인스턴스를 확장한 클래스와 비교할 가능성이 있기 때문에 Type Safety 하지 않아서 수정전 max가 이를 거부하는 것이다.

화면 캡처 2021-05-30 084720

타입 매개변수(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 하는 부분에서 에러가 난다.

화면 캡처 2021-05-30 085727

왜냐하면 비한정적 와일드타입인 <?>은 말그대로 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으로 선언해서 클라이언트에서 이러한 도우미 메서드가 있는지 알지 못하게 할 수 있는 장점이 있다.

댓글남기기