Search
🙈

이펙티브 자바:: 아이템 31 <한정적 와일드카드를 사용해 API유연성을 높이라>

Intro::

이펙티브 자바 정리본입니다.
앞서 아이템 28에서 정리하였듯이 매개변수화 타입은 불공변입니다. 예시로 List<String>은 List<Object>의 하위 타입이 아니라는 이야기입니다(리스코프 치환 원칙에 어긋나기 때문입니다).
하지만 때론 불공변 방식보다 유연한 무언가가 필요합니다.
public class Stack<E> { public Stack(); public void push(E e); public E pop(); public boolean isEmpty(); }
Java
복사
위 클래스에 일련의 원소를 스택에 넣는 매서드를 추가해야한다고 가정을 해봅시다.
public void pushAll(Iterable<E> src) { for (E e : src) { push(e); } }
Java
복사
이와 같은 경우 Iterable src의 원소 타입이 스택의 원소 타입과 일치해야만 동작하게 됩니다.

오류 예시

Stack<Number> numberStack = new Stack<>(); Iterable<Integer> integers = ...; numberStack.pushAll(numberStack);
Java
복사
매개변수화 타입이 불공변이기 때문에 해당 코드는 제대로 작동하지 않게됩니다.

펙스 적용 예시

한정적 와일드카드 타입

public void pushAll(Iterable<? extends E> src) { for (E e : src) { push(e); } }
Java
복사
위의 예제에서 src의 원소들을 사용해 내부 스택배열에 넣어주는 것이기 때문에 src를 생산자라고 볼 수 있습니다. 이를 통해 src 원소들은 stack 배열 원소의 하위 타입이어야 함을 알 수 있습니다.
public void popALl(Collection<? super E> dst) { while (!isEmpty()) { dst.add(pop()); } }
Java
복사
위의 예제에서는 dst에 stack 배열 원소가 들어가는 상황입니다. 때문에 dst를 소비자라고 볼 수 있습니다. 즉, dst의 원소 타입이 스택 배열의 원소 타입보다 상위 타입이어야 합니다.
// 적용 전 public static <E> Set<E> union(Set<E> s1, Set<E> s2) // 적용 후 public static <E> Set<E> union(Set<? extends E> s1, Set<? extends E> s2)
Java
복사
// 적용 전 public static <E extends Comparable<E>> E max(List<E> list) // 적용 후 public static <E extends Comparable<? super E>> E max(List<? extends E> list)
Java
복사
위의 예시는 조금 난해할 수 있다.
원래 선언에서는 E가 Comparable<E>를 확장한다고 정의하였는데, 이때 Comparable<E>는 E 인스턴스를 소비합니다. 때문에 매개변수화 타입 Comparable<E>를 한정적 와일드카드 타입인 Comparable<? super E>로 대체했습니다.

펙스(PECS): producer-extends, consumer-super

매개변수화 타입 T가 생산자라면 <? extends T>를 사용하고, 소비자라면 <? super T>를 사용해야 합니다.
유연성을 극대화하려면 원소의 생산자나 소비자용 입력 매개변수에 와일드카드 타입을 사용해야합니다.
반환 타입에는 한정적 와일드카드 타입을 사용하면 안됩니다. 유연성을 높여주기는커녕 클라이언트 코드에서도 와일드카드 타입을 써야 하기 때문입니다.
클래스 사용자가 와일드카드 타입을 신경 써야 한다면 그 API에 무슨 문제가 있을 가능성이 큽니다.

그래서 한정적 와일드카드 타입을 왜 써야하는가??

한정적 와일드카드를 쓰면서 복잡한 메서드를 만드는 것이 그 값어치를 할까라는 의문이 든다면, 그렇다고 합니다.
public static <E extends Comparable<E>> E max(List<E> list) { if (list.isEmpty()) { return null; } E max = list.get(0); for (int i = 1; i < list.size(); i++) { if (max.compareTo(list.get(i)) > 0) { max = list.get(i); } } return max; } public static <E extends Comparable<? super E>> E maxWildcard(List<? extends E> list) { if (list.isEmpty()) { return null; } E max = list.get(0); for (int i = 1; i < list.size(); i++) { if (max.compareTo(list.get(i)) > 0) { max = list.get(i); } } return max; } public static void main(String[] args) { List<ScheduledFuture<?>> futures = new ArrayList<>(); max(futures);// 컴파일에러: reason: Incompatible equality constraint: Delayed and ScheduledFuture<?> maxWildcard(futures); }
Java
복사
한정적 와일드카드 타입을 적용한 maxWildcard 메서드 에서만 사용할 수 있습니다. max 에서 해당 리스트를 처리하지 못하는 이유는 ScheduledFuture가 Comparable<ScheduledFuture>를 구현하지 않았기 때문입니다. ScheduledFuture는 Delayed의 하위 인터페이스이고, Delayed는 Comparable<Delayed>를 확장했습니다. 즉, ScheduledFuture의 인스턴스는 다른 ScheduledFuture 인스턴스 뿐 아니라 Delayed 인스턴스와도 비교할 수 있어서 max 메서드가 이 리스트를 거부하는 것입니다. 더 일반화해서 말하자면 Comparable을 직접 구현하지 않고, 직접 구현한 다른 타입을 확장한 타입을 지원하기 위해 와일드카드가 필요하다는 것입니다.

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

둘 중 무엇을 사용해야 할지에 대한 기본 규칙은 다음과 같습니다.
메서드 선언에 타입 매개변수가 한 번만 나오면 와일드카드로 대체해야합니다.
이때 비한정적 타입 매개변수라면 비한정적 와일드 카드로 바꾸고, 한정적 타입 매개변수라면 한정적 와일드카드로 바꾸면 됩니다.

References::

이펙티브 자바 / 조슈아 블로크 지음 (프로그래밍 인사이트)