아이템 31. 한정적 와일드카드를 사용해 API 유연성을 높이라. - ksw6169/effective-java GitHub Wiki

한정적 와일드카드를 사용하면 불공변으로 인한 문제를 유연하게 개선할 수 있다.

  • 매개변수화 타입은 불공변으로 서로 간에 상위 타입도 아니고 하위 타입도 아니다.
  • 예를 들어 List<Object>에는 어떤 객체든 넣을 수 있지만, List<String>에는 문자열만 넣을 수 있다.
  • 이는 상위 타입의 객체를 하위 타입의 객체로 치환해도 프로그램은 정상 동작해야 한다는 리스코프 치환 원칙에 위배되는 것이므로 List<String>List<Object>의 하위 타입이 아니다.
  • 하지만 때로는 매개변수화 타입을 사용할 때 상위 타입의 객체를 받거나 하위 타입의 객체를 받아 유연하게 처리해야 하는 상황이 있을 수 있다.
  • 이 때는 매개변수화 타입에 한정적 와일드카드를 사용하여 코드에 유연성을 부여할 수 있다.

유연성을 극대화하려면 와일드카드 타입을 사용하라.

아래는 Stack 클래스이다.

public class Stack<E> {
    public Stack();
    public void push(E e);
    public E pop();
    public boolean isEmpty();
}

여기에 일련의 요소를 스택에 넣는 메소드를 추가해야 한다고 했을 때

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

이를 사용하는 코드는 다음과 같을 수 있다.

Stack<Number> numberStack = new Stack<>();
Iterable<Integer> integers = ...;
numberStack.pushAll(integers);

IntegerNumber 의 하위 타입이니 잘 동작해야 할 것 같지만 실제로는 오류 메시지가 발생한다. 매개변수화 타입이 불공변이기 때문이다.

StackTest.java:7: error: incompatible types: Iterable<Integer>
cannot be converted to Iterable<Number>
        numberStack.pushAll(integers);
                            ^

이 때는 특별한 매개변수화 타입인 한정적 와일드카드 타입을 사용해 대처할 수 있다.

// E의 하위 타입을 받아 처리할 수 있도록 한정적 와일드카드 타입을 적용함
public void pushAll(Iterable<? extends E> src) {
    for (E e : src)
        push(e);
}

마찬가지로 아래와 같이 popAll() 이라는 메소드가 있다면

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

이를 사용하는 코드는 다음과 같을 수 있다.

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

하지만 pushAll() 때와 마찬가지로 컴파일을 하면 에러가 발생한다. 이를 해결하기 위해 한정적 와일드카드 타입을 적용하면 다음과 같다.

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

와일드카드 타입을 쓰지 말아야 하는 상황

  • 입력 매개변수가 생산자와 소비자 역할을 동시에 한다면 와일드카드 타입을 써도 좋을 게 없다.
  • 이 때는 타입을 정확히 지정해야 하는 상황으로 와일드카드 타입을 쓰지 말아야 한다.

PECS(Producer-Extends, Consumer-Super)

  • PECS 공식은 와일드카드 타입을 사용하는 기본 원칙이다.
  • PECS라는 공식을 외워두면 어떤 와일드카드 타입을 써야 하는지 기억하는 데 도움이 될 것이다.
  • PECS는 매개변수화 타입 T가 생산자라면 <? extends T> 를 사용하고, 소비자라면 <? super T> 를 사용하라는 공식이다.

PECS 공식을 적용해보자.

Case 1. Chooser 생성자

choices 컬렉션은 T 타입의 값을 생산하기만 한다.

public Chooser(Collection<T> choices)

이를 PECS 공식에 따라 개선하면 다음과 같다.

public Chooser(Collection<? extends T> choices)

Case 2. Set을 합치는 union()

s1과 s2 모두 E의 생산자다.

public static <E> Set<E> union(Set<E> s1, Set<E> s2)

이를 PECS 공식에 따라 개선하면 다음과 같다.

public static <E> Set<E> union(Set<? extends E> s1, Set<? extends E> s2)

수정한 메소드를 사용하는 클라이언트 코드는 다음과 같다.

Set<Integer> integers = Set.of(1,3,5);
Set<Double> doubles = Set.of(2.0, 4.0, 6.0);
Set<Number> numbers = union(integers, doubles);

주의 사항

반환 타입에는 한정적 와일드카드 타입을 사용하면 안된다. 유연성을 높여주기는 커녕 클라이언트 코드에서도 와일드카드 타입을 써야 하기 때문이다.


명시적 타입 인수(explicit type argument)

  • Java 7까지는 타입 추론 능력이 충분히 강력하지 못하다. 따라서 명시적 타입 인수를 추가해줘야 하는 경우가 있을 수 있다.
Set<Number> numbers = Union.<Number> union(integers, doubles);

타입 매개변수와 타입 인수

매개변수는 메소드 선언에 정의한 변수이고, 인수는 메소드 호출 시 넘기는 실젯값이다. 이는 제네릭에서도 동일하게 적용되어 Set<T> 에서는 T 를 타입 매개변수라 하며, 이를 사용하는 Set<Integer> 에서 Integer 는 타입 인수라 한다.


참고 자료

  • Effective Java 3/E
⚠️ **GitHub.com Fallback** ⚠️