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

Posted by yunki kim on February 11, 2023

  매개변수화 타입은 불공변(invariant)이다. 즉, 서로 다른 타입 Type1과 Type2가 있을 때 List <Type1>과 List <Type2>는 서로의 상위 타입도 하위 타입도 아니다. 예컨대 List <String>은 List <Object>의 하위 타입이 아니다. 이는 List <String>이 List <Object>가 하는 일을 제대로 수행하지 못하는 어쩌면 당연한 일이다.

  하지만 때로는 불공변보다 유연한 방식이 필요하다. 예를 들어 다음과 같은 코드르 보자.

1
2
3
4
5
6
7
8
9
public class Stack<E> {
    public Stack();
    public void push(E e);
    public E pop();
    public boolean isEmpty();
    public void pushAll(Interable<E> src);
}
 
 
cs

  위 코드에서 pushAll() 메서드는 완벽하지 않다. 예를 들어 Stack<Number>에서 int 타입을 pushAll()의 파라미터로 넘긴다 해보자. 그러면 매개변수화 타입은 불공변이므로 에러가 발생한다.

  위 같은 문제를 해결하기 위해 한정적 와일드카드 타입이라는 매개변수화 타입을 지원한다. 한정적 와일드카드 타입은 상한(<? extends E>), 하한(<? super E>)으로 구분된다. 상한은 E의 하위 타입 요소를 모두 받는다. 하한은 반대로 E의 상위 타입 요소를 모두 받는다. 따라서 pushAll()의 선언을 다음과 같이 수정하면 int 타입도 받을 수 있게 된다.

1
void pushAll(Interable<extends E> src);
cs

  모든 원소를 pop하는 메서드의 경우 다음과 같이 정의할 수 있다.

1
void popAll(Coolection<super E> dst)
cs

  유연성을 극대화하려면 원소의 생산자나 소비자용 입력 매개변수에 와일드카드 타입을 사용하라. 만약 입력 매개변수가 동시에 소비자와 생산자의 역할을 한다면, 이는 타입을 정확히 지정해야 하는 상황이므로 와일드카드를 사용하지 말라.

  와일드카드 타입을 고를 때 PECS(producer-extends, consumer-super) 공식을 기억하면 도움이 된다. 이 공식으로 위 예시를 보면, pushAll은 Stack이 사용할 E 인스턴스를 생성 하기에 extends가, popAll은 Stack으로부터 E 인스턴스를 소비하기에 super가 적절하다.

  반환 타입에는 한정적 와일드카드를 사용하지 않도록 주의하자. 반환값에 한정적 와일드카드를 사용한다면 유연성을 높여주지도 않고 클라이언트 코드에서도 와일드카드를 사용해야 한다. 클래스 사용자가 와일드카드 타입을 신경써야 한다면 그 API에 문제가 있을 가능성이 크다.

  다음과 같은 코드가 존재한다 해보자.

1
2
3
public static <E> Set<E> union(Set<E> s1, Set<E> s2)
 
set<Number> numbers = union(integers, doubles);
cs

  위 코드는 자바7 이하에서는 "incompatible types"라는 에러를 발생시킨다. 이는 목표 타이핑(target typing)을 java 8 부터 지원하기에 컴파일러가 올바른 타입 추론을 하지 못해 발생하는 에러다. 이를 해결하기 위해선 명시적 타입 인수(explicit type arguemnt)를 사용하면 된다.

1
set<Number> numbers = Union.<Number>union(integers, doubles);
cs