[교재 EffectiveJava] 아이템 31. 한정적 와일드카드를 사용해 API 유연성을 높이라

반응형
728x90
반응형

불공변 타입

매개변수화 타입은 불공변(invariant)이다. 즉, 서로 다른 타입 Type1, Type2 가 있을때 List<type1>은 List<Type2>의 하위 타입도 상위 타입도 아니다. 예를들어, List<String> 은 문자열만 넣을 수 있고, List<Object>는 어떤 객체도 넣을 수 있기 때문에 List<String>은 List<Object>의 하위타입이 아니다. 이는 List<String>이 List<object>가 하는 일을 제대로 수행하지 못하므로 하위 타입이 될 수 없다는 결론이다. 

 

 

와일드 카드 타입으로의 변경

Stack 의 public API 추린 코드
public class Stack<T> {
    public Stack();
    public void push (E e);
    public E pop();
    public boolean isEmpty();
}

 

여기에 일련의 원소를 스택에 넣는 메서드를 추가해야한다고 해보자.

public void pushAll(Iterable<E> src) {
    for (E e : src) {
        push(e);
    }
}

 

이 메서드는 경고 없이 컴파일 되지만, 완벽하지 않은 코드다. Iterable src의 원소 타입이 Stack의 원소 타입과 일치하면 잘 작동한다. 하지만 Stack<Number>로 선언한 후 pushAll(Integer 타입의 매개변수) 을 호출하면 Integer는 Number의 하위 타입이므로 잘 동작할 것이라고 생각이 들 것이다. 하지만 실제로는 오류가 발생한다. 

 

오류 발생 코드
Stack<Number> numberStack = new Stack<>();
Iterable<Integer> integers = ...;
numberStack.pushAll(integers);

 

매개변수화 타입이 불공변하기 때문이다. 이를 해결하기 위해서는 pushAll의 입력 매개변수 타입이 'E의 Iterable' 이 아니라 'E의 하위 타입의 Iterable'이어야 하며, 와일드카드 타입 Iterable<? extneds E>로 사용해야한다. 

Number의 경우 Iterable<? extends Number>로 사용하면, Iterable<Integer>, Iterable<Long> 등이 모두 가능하게된다.

 

와일드카드 타입 사용 코드
public void pushAll(Iterable<? extends E> src) {
    for (E e : src) {
        push(e);
    }
}

 

또다른 예시로, popAll 메서드도 살펴보자.

popAll 메서드
public void popAll(Collection<E> dst) {
    while (!isEmpty()) {
        dst.add(pop());
    }
}

 

이번에도 Stack<Number>의 원소를 Object 용 컬렉션으로 옮기려한다고 생각해보자.

Stack<Number> numberStack = new Stack<>(); 
Collection<Object> objects = new ArrayList<>(); 
numberStack.popAll(objects);

 

"Collection<Object>는 Collection<Number>의 하위 타입이 아니다"라는 오류 코드가 나온다. 이번에도 와일드카드 타입으로 해결할 수 있다. 이번에는 popAll의 입력 매개변수 타입이 'E의 Collection'이 아니라 'E의 상위 타입의 Collection'이여야 한다.

Number의 경우 Collection<? super Number>로 사용하면, Collection<Number>, Collection<Object> 등이 모두 가능하게된다.

 

와일드카드 타입 적용 (아래 코드의 의미 : 모든 타입은 자기 자신의 상위 타입이다.)
public void popAll(Collection<? super E> dst) { 
    while (!isEmpty()) {
        dst.add(pop()); 
    }
}

 

이제 말끔하게 컴파일된다. 유연성을 극대화하려면 원소의 생산자나 소비자용 입력 매개변수에 와일드카드 타입을 사용해야한다. 타입을 정확하게 지정해야하는 상황이라면 와일드카드 타입의 사용을 하지말자. 

 

 

PECS 공식

팩스 (PECS) : producer-extends, consumer-super
매개변수화 타입 T가 생산자라면?
<? extends T>


매개변수화 타입 T가 소비자라면?
<? super T>

 

위 예제에서 본다면, pushAll 의 src 매개변수는 Stack 이 사용할 E 인스턴스를 생산하므로 생산자의 경우다. 또, popAll 의 src 메서드는 Stack 으로부터 E 인스턴스를 소비하므로 소비자의 경우에 속한다. PECS 공식은 와일드카드 타입을 사용하는 기본 원칙이다. 

 

예제코드
public Chooser(Collection<T> choices)

 

choincs 컬렉션은 T 타입의 값을 생산하기만 한다. 그러므로 T를 확장하는 와일드카드 타입을 사용하여 선언해야한다.

 

public Chooser(Collection<? extends T> choices)

 

이렇게 변경함으로써 생기는 차이는, Chooser<Number>의 생성자에 List<Integer>를 넘기고 싶을 경우 와일드카드 타입으로 변경함으로써 컴파일 에러 없이 정상적으로 작동된다. 

 

 

PECS 공식 2번 적용

before
public static <E extends Comparable<E>> E max(List<E> list)

 

after
public static <E extends Comparable<? super E>> E max(List<? extends E> list)

 

 

입력 매개변수에서는 E 인스턴스를 생산하므로 원래의 List<E>를 List<? extends E>로 수정했다. 타입 매개변수 E를 보면, E가 Comparable<E>를 확장한다고 되어있었는데, 이때 Comparable<E>는 E 인스턴스를 소비한다. 그래서 매개변수화 타입 Comparable<E>를 한정적 와일드카드 타입인 Comparable<? super E>로 대체했다. Comparable 은 언제나 소비자이므로, 일반적으로 Comparable<E> 보다는 Comparable<? super E>를 사용하는 편이 낫다. 

 

after 코드에 매개변수로 넘길 List
List<ScheduledFuture<?>> scheduledFutures = ...;

 

위 리스트는 수정된 max 메서드에서만 처리할 수 있다. 수정 전 max는 java.util.concurrent.ScheduledFuture 가 Comparable<ScheduledFuture>를 구현하지 않았기 때문이다. ScheduledFuture 는 Delayed 의 하위 인터페이스이고, Delayed 는 Comparable<Delayed>를 확장했다. 

public interface ScheduledFuture<V> extends Delayed, Future<V> {}
public interface Delayed extends Comparable<Delayed> {}
public interface Comparable<T> {}

 

ScheduledFuture의 인스턴스는 다른 ScheduledFuture의 인스턴스뿐 아니라 Delayed 인스턴스와도 비교할 수 있어서 수정전 max가 이 리스트를 거부하는 것이다. 

 

수정전 max
public static <E extends Comparable<E>> E max(Collection<E> collection) {}

 

여기서, Comparable 을 직접 구현하지 않고, 직접 구현한 다른 타입을 확장한 타입을 지원하기 위해 와일드카드가 필요하다.

 

 

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

어떤 경우에는 타입 매개변수와 와일드카드에 공통되는 부분이 있어서, 메서드를 정의할때 둘중 어느것을 사용해도 괜찮을 때가 많다.

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

 

public API 라면 와일드카드를 사용한 두번째 swap 메서드가 더 낫다. 어떤 리스트는 이 메서드에 넘기면 명시한 인덱스의 원소들을 교환해주기 때문이다. 이렇듯, 메서드 선언에 타입 매개변수가 한번만 나오면 와일드카드로 대체하자. 비한정적 타입 매개변수의 경우에는 비한정적 와일드카드로 바꾸고, 한정적 타입 매개변수라면 한정적 와일드카드로 바꾸면 된다. 

 

하지만, 두번째 코드에 아래의 경우 코드가 컴파일 되지 않을 수 있다.

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

 

list.get(i) 로 얻은 원소를 리스트에 다시 넣을 수 없다는 오류다. 이유는 List<?> 인데, List<?>에는 null 이외에 어떤 값도 넣을 수 없기 때문이다. 이 문제를 해결하기위해 와일드카드 타입의 실제 타입을 알려주는 메서드를 private 도우미 메서드로 따로 추가하자.

public static void swap(List<?> list, int i, int j) {
    // list.set(i, list.set(j, list.get(i))); // error
    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 타입의 값이라면 이 리스트에 넣어도 안전하다는 사실을 알고있다. 이렇게 함으로써 swap 메서드를 호출하는 클라이언트는 swapHelper의 존재를 모른채 정상적으로 실행될 수 있다.

 

 

 

 

반응형

Designed by JB FACTORY