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

들어가며

매개변수화 타입은 불공변이다. 하지만 때론 불공변 방식보다 유연한 무언가가 필요하다. 이왕이면 제네릭 타입으로 만들라에서 만든 Stack 클래스에 pushAll이라는 메소드를 추가해야 한다고 가정하자.

1
2
3
4
public void pushAll(Iterable<E> src) {
for (E e : src)
push(e);
}
1
2
Stack<Number> numberStack = ... ;
numberStack.pushAll(some Integer type value);

위 코드에서 Integer는 Number의 하위 타입이니 논리적으로 잘 동작해야 할 것 같지만 오류가 나온다. 이는 매개변수화 타입은 불공변이기 때문이다. 이럴 때는 한정적 와일드카드 타입이라는 매개변수화 타입을 이용하면 된다. 즉 pushAll의 입력 매개변수 타입은 E의 Iterable이 아닌 E의 하위 타입의 Iterable이여야 한다는 말이고 이를 표현하면 Iterable<? extends E>와 같다.

1
2
3
4
public void pushAll(Iterable<? extends E> src) {
for(E e : src)
push(e);
}

다음은 pushAll과 짝을 이루는 popAll이다.

1
2
3
4
public void popAll(Collection<E> dst) {
while(!isEmpty())
dst.add(pop());
}
1
2
3
Stack<Number> numberStack = ... ;
Collection<Object> objects = ... ;
numberStack.popAll(objects);

위 또한 불공변이기 때문에 오류가 발생한다. 이 또한 와일드카드 타입으로 해결할 수 있다. 입력 매개변수의 타입을 E의 상위 타입의 Collection으로 변경하면 된다(모든 타입은 자기 자신의 상위 타입이다). 즉 와일드카드 타입으로 표현하면 Collection<? super E>이다.

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

메세지는 분명하다. 유연성을 극대화하려면 원소의 생산자나 소비자용 입력매개변수에 와일드카드 타입을 사용하라. 다만 입력 매개변수가 생산자와 소비자 역할을 동시에 한다면 와일드카드 타입을 사용해도 의미가 없다.

PECS

펙스(PECS) : producer-extends, consumer-super
즉, 매개변수화 타입 T가 생산자라면 <? extends T>를 사용하고 소비자라면 <? super T>를 사용하라. PECS 공식은 와일드카드 타입을 사용하는 기본 원칙이다. 나프탈린과 와들러는 이를 Get and Put Principle이라 칭한다.

이왕이면 제네릭 메소드로 만들라에서 만든 union 메소드 또한 PECS 공식을 적용할 수 있다.

1
2
3
4
5
public static <E> Set<E> union(Set<? extends E> s1, Set<? extends E> s2) {
Set<E> result = new HashSet<>(s1);
result.addAll(s2);
return result;
}
1
2
3
4
5
Set<Integer> integers = new HashSet<>(Arrays.asList(1, 3, 5));
Set<Double> doubles = new HashSet<>(Arrays.asList(2.0, 4.0, 6.0));
Set<Number> numbers = union(integers, doubles);
//Java7에서는 타입 추론이 강력하지 않기 때문에 명시적 타입 인수를 사용해야 한다!
Set<Number> numbersJava7 = main.<Number>union(integers, doubles);

max 메소드 또한 똑같다.

1
2
3
4
5
6
7
8
9
10
11
public static <E extends Comparable<? super E>> E max(Collection<? extends E> c) {
if (c.isEmpty())
throw new IllegalArgumentException("Empty Collection");

E result = null;
for (E e : c)
if (result == null || e.compareTo(result) > 0)
result = Objects.requireNonNull(e);

return result;
}

인스턴스를 생산하므로 Collection<E>보다 Collection<? extends E>가 더 낫다. 그리고 Comparable은 언제나 소비자이므로 Comparable<E>보다 Comparable<? super E>가 더 낫다. 이제 이 max는 다음과 같은 코드를 처리할 수 있다.

1
List<ScheduledFuture<?>> scheduledFutures = ... ;

수정전 max는 ScheduledFutre가 Comparable<ScheduledFuture>를 구현하지 않기 때문에 이를 처리할 수 없었다. 하지만 ScheduledFuture는 Delayed의 하위 인터페이스이고 Delayed는 Comparable<Delayed>를 확장했기 때문에 이제 ScheduledFutre는 ScheduledFutre뿐만 아니라 Delayed 인스턴스와도 비교할 수 있어서 사용이 가능하다.

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

타입 매개변수와 와일드카드는 공통분모가 많아 어느 것을 사용해도 괜찮을 때가 많다. 다음 코드를 보자. 첫번째는 비한정적 타입 매개변수를, 두번째는 비한정적 와일드카드를 사용했다.

1
2
public static <E> void swap(List<E> list, int i, int j);
public static void swap(List<?> list, int i, int j);

어느것이 더 나아보이는가. public API라면 간단한 두번째가 좋다. 어떤 리스트든 이 메소드에 넘기면 두 원소를 교환해준다. 신경 써야 할 타입 매개변수도 없다. 기본 규칙은 다음과 같다. 메소드 선언에 타입 매개변수가 한 번만 나오면 와일드 카드로 대체하라.

하지만 두번째 코드는 문제가 있다. 다음의 코드는 컴파일되지 않는다.

1
2
3
public static void swap(List<?> list, int i, int j) {
list.set(i, list.set(j, list.get(i)));
}

원인은 List\<?>에는 null외에는 아무것도 넣을 수 없다는데 있다. 이럴때 로 타입을 사용하지 않고서 해결하는 방법이있다. private helper method를 작성하자. 실제 타입을 알기 위해선 helper method는 제네릭 메소드여야 한다.

1
2
3
4
5
6
7
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)));
}

덕분에 외부에서는 와일드카드 기반의 선언을 유지하며 동작을 할 수 있다.

Author: Song Hayoung
Link: https://songhayoung.github.io/2020/08/11/Languages/Effective%20JAVA/item31/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.