Item 31. Wildcard Type

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

item 28에서 언급했듯 매개변수화 타입은 불공변(invariant)인 부분에 대해 의문점을 가질 수 있다. List<String>List<Object>의 하위 타입이 아닌데, List<String>List<Object>가 하는 일을 제대로 수행할 수 없기 때문에 리스코프 치환 원칙에 따르면 불공변인 것이 타당하다.

하지만 이대로 사용하기엔 불편한 점이 많아 매개변수화 타입을 유연하게 사용할 수 있도록 도와주는 방법이 있는데, 바로 한정적 와일드카드 타입(bounded wildcard type)을 사용하는 것이다.

import java.util.Collection;

class Stack<E> {

    // ...

    public void push(E e) {
        elements[size++] = e;
    }

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

    public void pop(E e) {
        E result = elements[--size];
        elements[size] = null;
        return result;
    }

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

class Main {

    public static void main(String[] args) {
        Stack<Number> numberStack = new Stack<>();
        Iterable<Integer> integers = ...;
        numberStack.pushAll(integers); // 컴파일 에러, Iterable<Integer>는 Iterable<Number>의 하위 타입이 아님

        Collection<Object> objects = ...;
        numberStack.popAll(objects); // 컴파일 에러, Collection<Object>는 Collection<Number>의 하위 타입이 아님
    }
}

Integer는 Number의 하위 타입이니 논리적으로 잘 동작할 것 같지만, 제네릭의 불공변 특성으로 인해 컴파일 에러가 발생한다. 해결책으로는 앞의 아이템들에서 언급했듯이 한정적 와일드카드 타입을 사용하는 것이다.

class Stack<E> {

    // ...

    public void push(E e) {
        elements[size++] = e;
    }

    public void pushAll(Iterable<? extends E> src) { // 한정적 와일드카드 타입 적용
        for (E e : src) {
            push(e);
        }
    }

    public void pop(E e) {
        E result = elements[--size];
        elements[size] = null;
        return result;
    }

    public void popAll(Collection<? super E> dst) { // 한정적 와일드카드 타입 적용
        while (!isEmpty()) {
            dst.add(pop());
        }
    }
}
  • Iterable<? extends E>

E의 하위 타입을 모두 포함하는 Iterable 타입을 매개변수로 받을 수 있게 되어, Number의 하위 타입인 Integer를 포함하는 Iterable 타입을 매개변수로 받을 수 있게 되었다.

  • Collection<? super E>

E의 상위 타입을 모두 포함하는 Collection 타입을 매개변수로 받을 수 있게 되어, Number의 상위 타입인 Object를 포함하는 Collection 타입을 매개변수로 받을 수 있게 되었다.

이처럼 한정적 와일드카드 타입을 사용하면 매개변수화 타입이 불공변이라도 유연하게 사용할 수 있게 된다.

PECS(Producer-Extends, Consumer-Super)

한정적 와일드카드 타입을 사용하면 매개변수화 타입이 불공변이라도 유연하게 사용할 수 있게 되었지만, 이를 사용할 때 주의할 점이 있다. 바로 생산자(producer)와 소비자(consumer) 역할에 따라 extendssuper를 적절히 사용해야 한다는 것이며, 그 원칙은 다음과 같다.

  • 매개변수화 타입 T가 생산자인 경우: <? extends T>

  • 매개변수화 타입 T가 소비자인 경우: <? super T>

  • 매개변수화 타입 T가 생산자와 소비자인 경우: T

  • 반환 타입: 한정적 와일드카드 타입 적용 X, 클라이언트 코드에 와일드카드 타입이 전파되기 때문에 클라이언트 코드가 더 복잡해짐

앞서 언급한 Stack 예제에서도 생산자(pushAll)와 소비자(popAll) 역할에 따라 extendssuper를 사용했다. 위 두 상황이 아닌, 입력 매개변수가 생산자와 소비자 역할을 동시에 해야 하는 상황에선 와일드카드 타입을 쓰지 않는 것이 좋다.

위 공식은 item 30에서 살펴보았던 Collections.max 메서드에도 이미 적용되어 있음을 알 수 있다.

// Comparable 인터페이스
public interface Comparable<T> {
    int compareTo(T o);
}

// java.util.Collections의 max 메서드
public class Collections {
    // Suppresses default constructor, ensuring non-instantiability.
    private Collections() {
    }

    // ...

    public static <T extends Object & Comparable<? super T>> T max(Collection<? extends T> coll) {
        Iterator<? extends T> i = coll.iterator();
        T candidate = i.next();

        while (i.hasNext()) {
            T next = i.next();
            if (next.compareTo(candidate) > 0)
                candidate = next;
        }
        return candidate;
    }

    // ...
}
  • Collection<? extends T> coll: 입력 매개변수에서 생산자 역할을 하므로 extends를 사용

  • Comparable<? super T>: 입력 매개변수를 소비하면서 compareTo 메서드를 호출하므로 super를 사용

타입 매개변수 vs 와일드카드

타입 매개변수와 와일드카드는 타입 다형성에 있어 공통되는 부분이 있어 둘 중 하나를 선택해 사용할 수 있는 경우가 있다.

interface Swap {
    // 1. 타입 매개변수를 사용한 메서드 선언
    public static <E> void swap(List<E> list, int i, int j);

    // 2. 와일드카드를 사용한 메서드 선언
    public static void swap(List<?> list, int i, int j);
}

위 처럼 메서드 선언에 타입 매개변수가 한 번만 나오는 경우엔 와일드카드로 대체하는 것이 좋은 방법이 될 수 있다. public API라면 두 번째 메서드가 어떤 타입의 List도 받을 수 있기 때문에 더 유연하게 사용할 수 있기 때문이다.

이를 구현하기 위해 추가적인 helper 메서드를 구현해야 한다는 단점이 있지만, 클라이언트는 이를 알 필요가 없으므로 문제가 되지 않는다.

// 직관적으로 구현한 swap 메서드
class Swap1 {

    public static void swap(List<?> list, int i, int j) {
        list.set(i, list.set(j, list.get(i))); // 컴파일 에러, List<?>에는 null 외에는 어떤 값도 넣을 수 없음
    }
}

// helper method 추가
class Swap2 {

    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)));
    }
}

Last updated

Was this helpful?