[Effective Java] 아이템31 - 한정적 와일드카드를 사용해 API 유연성을 높이라
아이템28에서 살펴봤듯이 parametrized type은 invariant이다. 서로 다른 Type1, Type2가 있을 때, List
제너릭 사용 시, 이러한 invariant 성격 때문에 메소드 선언 시 하위타입의 원소를 같이 사용하지 못하는데, 이를 유연하게 사용할 수 있는 방법이 한정적 와일드카드 타입 (Bounded Wildcard Type) 이다.
와일드카드 타입을 사용하지 않은 pushAll 메서드
public void pushAll(Iterable<E> src) {
for(E e : src) {
push(e);
}
}
Stack<Number> numberStack = new Stack<>(); // Number 타입 스택 생성
Iterable<Integer> integers = ...;
numberStack.pushAll(integers);
// 에러! incompatible types : Iterable<Integer> cannot be converted to Iterable<Number>
Number
는 Integer
의 하위타입인데도 불구하고 Number
타입으로 선언된 Stack에 Integer 속성을 넣지 못한다. (vice versa도 마찬가지 일 것이다.)
와일드카드 타입을 사용한 pushAll 메서드
public void pushAll(Iterable<? extends E> src) {
for (E e : src) {
push(e);
}
}
<? extends E>
: E의 하위 타입의 Iterable이어야 한다는 뜻이다.
pushAll
메서드는 input parameter인 src로부터 원소를 옮겨 담는 코드이다. 따라서 입력 파라미터를 producer라고 할 수 있다.
반대로, 메서드 안의 원소를 input paramter로 옮겨 담아서 전달한다면, 그 입력 파라미터는 consumer라고 부른다.
다음 consumer 역할을 하는 메서드를 보자.
와일드카드 타입을 사용하지 않은 popAll 메서드
public void popAll(Colllection<E> dst) {
while(!isEmpty()) {
dst.add(pop());
}
}
위와 같이, 원소를 꺼내서 입력 파라미터에 전달하는 경우, E
가 스택의 원소 타입과 동일하면 문제가 되지 않는다.
하지만 아래와 같이, Stack
Stack<Number> numberStack = new Stack<>();
Collection<Object> objects = ...;
numberStack.popAll(objects);
Collection
와일드카드 타입을 사용한 popAll 메서드
public void popAll(Collection<? super E> dst) {
while(!isEmpty()) {
dst.add(pop());
}
}
Collection<? super E>
: E의 상위타입의 Collection이어야 한다 라는 뜻이다. 따라서 앞에서 에러가 났던 부분이 해결이 된다.
이처럼 유연성을 극대화하려면 producer나 consumer input paramter에 wildcard type을 사용하자.
앞서 예제에서 봤듯이, producer은 extends
를 썼고, consumer는 super
을 사용했다. 이는 공식이다.
PECS : producer-extends, consumer-super
PECS 구체적 예
- union 메서드
public static <E> Set<E> union(Set<E> s1, Set<E> s2)
위와 같이 두 개의 Set
을 받아서 하나의 Set
으로 합치는 메서드가 있다고 가정할 때, s1 과 s2 모두 E
의 producer이니 extends
를 쓰면 된다.
public static <E> Set<E> union(Set<? extends E> s1, Set<? extends E> s2)
return type은 여전히 Set<E>
인 것에 주목하자. 반환 타입에는 한정적 와일드카드 타입을 사용하면 안된다.
- 위 코드는 java8 부터 제대로 컴파일 된다. java7 까지는 타입 추론 능력이 강력하지 못해서 타입을 명시해야 한다.
// Union.class의 union 메서드
Set<Number> numbers = Union.<Number> union(integers, doubles);
- max 메서드
와일드카드 타입 사용 전
public static <E extends Comparable<E>> E max(List<E> list)
와일드카드 타입 사용 후
public static <E extends Comparable<? super E>> E max(List<? extends E> list)
- List
list 에서 List<? extends E> 로 변경했다. E 인스턴스를 생산해서 사용하기 떄문이다. - Comparable
가 E 인스턴스를 소비해서 선후 관계를 뜻하는 정수를 생산하기 때문에 super 와일드카드 타입을 사용했다. Comparable
,Comprator
은 언제나 consumer 임을 기억하자.
이렇게 복잡하더라도 와일드카이드 타입은 사용할만한 가치가 있다. 왜냐하면, 상위클래스/하위클래스 사이에 복잡한 관계가 있을 수 있기 때문이다.
다음 리스트는 수정된 max
로만 처리할 수 있다.
List<ScheduledFuture<?>> scheduledFutures = ...;
ScheduledFuture
이 Comparable<ScheduledFuture>
를 구현하지 않았기 때문에 수정된 max 메서드에서는 실행이 불가하다.
아래 사진과 같이, ScheduledFuture은 Delayed의 하위 인터페이스인데, 이 Delayed는 Comprable
다시말해, ScheduledFuture이 다른 Delayed 인스턴스를 확장한 클래스와 비교할 가능성이 있기 때문에 Type Safety 하지 않아서 수정전 max가 이를 거부하는 것이다.
타입 매개변수(Parametrized Type)과 와일드카드(Wild Type)
<T>
나 <E>
처럼 타입 매개변수를 받는 것과 <?>
처럼 와일드카드 타입을 사용하는 것은 어떻게 보면ㅌ 비슷해보인다.
만약 메서드 선언에 타입 매개변수가 한 번만 나오면 와일드카드로 대체하자.
public static <E> void swap(List<E> list, int i, int j);
public static void swap(List<?> list, int i, int j); // 한번밖에 안나왔으니, 이렇게 쓰자
private helper 메서드
하지만 아래와 같은 두번째 swap
메소드는 실제로 컴파일이 되지 않는다.
public static void swap(List<?> list, int i, int j) {
list.set(i, list.set(j, list.get(i)));
}
list.get(i)를 가져와서 list에 다시 set 하는 부분에서 에러가 난다.
왜냐하면 비한정적 와일드타입인 <?>
은 말그대로 Unknown Type이므로, list.get(i) 을 해온 타입이 <?>
알수없는 타입의 하위타입인지 알 방법이 없기 때문이다.
따라서, 와일드카드 타입의 실제 타입을 알려주는 메서드를 private 도우미 메서드로 따로 작성해서 활용한다.
public static void swap(List<?> list, int i, int j) {
swapHelper(list, i, j);
}
// 와일드카드 타입을 실제 타입으로 바꿔주는 private 도우미 메서드
private static <E> void swapHelper(List<E> list, int i, int j) {
list.set(i, list.set(j, list.get(i)));
}
swapHelper
메서드는 제네릭 메서드로, 컴파일 시점에는 이 리스트에서 꺼낸 값은 E이고, set할때도 E임을 알고 있다. 또한 private으로 선언해서 클라이언트에서 이러한 도우미 메서드가 있는지 알지 못하게 할 수 있는 장점이 있다.
댓글남기기