한정적 와일드카드를 사용해 API 유연성을 높이라
와일드카드 타입
매개변수화 타입은 불공변이다. 서로 다른 타입 Type1과 Type2가 있을 때 List<Type1>
은 List<Type2>
의 하위 타입도 상위 타입도 아니다.
와일드카드 타입을 사용하지 않은 메서드
public void pushAll(Iterable<E> src) {
for(E e : src)
push(e);
}
Stack<Number>
로 선언한 후 pushAll(intVal)
Integer 값을 호출한다면 Integer는 Number의 하위 타입이니 잘 동작해야 할 것 같지만 실제로는 오류 메시지가 뜬다. 매개변수화 타입이 불공변이기 때문이다.
public void popAll(Collection<E> dst) {
while (!isEmpty())
dst.add(pop());
}
Stack<Number>
의 원소를 Object용 컬렉션으로 옮기려 한다. 이 경우 ‘Collection<Object>
는 Collection<Number>
의 하위 타입이 아니다.`라는 pushAll과 비슷한 오류가 발생한다.
E 생산자 매개변수에 와일드카드 타입 적용
public void pushAll(Iterable<? extends E> src) {
for (E e : src)
push(e);
}
한정적 와일드카드 타입을 적용하여 ‘E의 하위 타입의 Iterable’이어야 한다고 정의하여 타입 안전하게 만들 수 있다.
public void popAll(Collection<? super E> dst) {
while (!isEmpty())
dst.add(pop());
}
마찬가지로 popAll의 입력 매개변수의 타입이 ‘E의 Collection’이 아니라 ‘E의 상위 타입의 Collection’이어야 한다고 정의한다.
유연성을 극대화하려면 원소의 생산자나 소비자용 입력 매개변수에 와일드카드 타입을 사용하자. 한편, 입력 매개변수가 생산자와 소비자 역할을 동시에 한다면, 타입을 정확히 지정해야 하는 상황으로, 와일드카드 타입을 쓰지 말아야 한다.
PECS : producer-extends, consumer-super
매개변수화 타입 T가 생산자라면<? extends T>
를 사용하고, 소비자라면<? super T>
를 사용하자. 이 공식은 와일드카드 타입을 사용하는 기본 원칙이다.
Comparable과 Comparator는 모두 소비자에 해당한다. 일반적으로 PECS 원칙에 따라 한정적 와일드카드 타입을 사용하여 Comparable<E>
보다는 Comparable<? super E>
, Comparator<E>
보다는 Comparator<? super E>
를 사용하는 편이 낫다.
반환 타입에는 한정적 와일드카드 타입을 사용하면 안 된다. 유연성을 높여주기는커녕 클라이언트 코드에서도 와일드카드 타입을 써야 하기 때문이다.
클래스 사용자가 와일드카드 타입을 신경 써야 한다면 그 API에 무슨 문제가 있을 가능성이 크다.
타입 매개변수와 와일드카드
타입 매개변수와 와일드카드에는 공통되는 부분이 있어서, 메서드를 정의할 때 둘중 어느 것을 사용해도 괜찮을 때가 많다.
// 비한정적 타입 매개변수
public static <E> void swap(List<E> list, int i, int j);
// 비한정적 와일드카드
public static void swap(List<?> list, int i, int j);
public API일 경우 간단한 비한정적 와일드카드가 낫다. 메서드 선언에 타입 매개변수가 한 번만 나오면 와일드 카드로 대체하라. 이 때 비한정적 타입 매개변수라면 비한정적 와일드카드로 바꾸고, 한정적 타입 매개변수라면 한정적 와일드카드로 바꾸면 된다.
비한정적 와일드카드 도우미 메서드
와일드카드를 사용하면 방금 꺼낸 원소를 리스트에 다시 넣을 수 없다. 원인은 리스트의 타입 List<?>
는 null 외에는 어떤 값도 넣을 수 없다는 데 있다. 이 경우 와일드카드 타입의 실제 타입을 알려주는 메서드를 private 도우미 메서드로 따로 작성하여 활용할 수 있다.
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)));
}
swapHelper 메서드는 리스트가 List<E>
임을 알고 있다. 즉, 이 리스트에서 꺼낸 값의 타입은 항상 E이고, E 타입의 값이라면 이 리스트에 넣어도 안전함을 알고 있다.
와일드카드 타입을 적용하면 API가 훨씬 유연해진다. 생산자는 extends를 소비자는 super를 사용한다.
Comments