본문 바로가기

독서찰기(讀書札記)/이펙티브 자바

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

유연성을 극대화하려면 원소의 생산자나 소비자용 입력 매개변수에 와일드카드 타입을 사용하라.

[Why]

매개변수화 타입은 불공변(invariant)이다. (아이템28)

  • 서로 다른 타입 Type1과 Type2가 있을 때 List<Type1>은 List<Type2>의 하위 타입도 상위 타입도 아니다.
  • List<String>은 List<Object>의 하위 타입이 아니라는 뜻인데, 곰곰이 따져보면 사실 이쪽이 말이 된다.
  • List<String>은 List<Object>가 하는 일을 제대로 수행하지 못하니 하위 타입이 될 수 없다.
    (리스코프 치환 원칙에 어긋난다. 아이템10)

하지만 때론 불공변 방식보다 유연한 무언가가 필요하다.

public void pushAll(Iterable<E> src) {
    for (E e : src) {
        push(e);
    }
}
  • 일련의 원소를 스택에 넣는 메서드이다.
  • 이 메서드는 깨끗이 컴파일되지만 완벽하지 않다. 
    • Iterable src의 원소 타입이 스택의 원소 타입과 일치하면 잘 작동한다.
    • 하지만 Stack<Number>로 선언한 후 pushAll(intVal)을 호출하면 어떻게 될까?
    • Integer는 Number의 하위 타입이니 잘 동작해야 할 것 같지만, 오류 메시지가 뜬다. incompatible types
    • 매개변수화 타입이 불공변이기 때문이다. 

 

[How]

자바는 이런 상황에 대처할 수 있는 한정적 와일드카드 타입이라는 특별한 매개변수화 타입을 지원한다.

public void pushAll(Iterable<? extends E> src) {
    for (E e : src) {
        push(e);
    }
}
  • pushAll의 입력 매개변수 타입은 'E의 Iterable'이 아니라 'E의 하위 타입의 Iterable'이어야 하며,
    와일드 카드 타입 Iterable<? extends E>가 정확히 이런 뜻이다.
    (사실 extends라는 키워드는 이 상황에 딱 어울리지는 않는다. 하위 타입이란 자기 자신도 포함하지만 그렇다고 자신을 확장한 것은 아니기 때문이다. 아이템29)
public void popAll(Collection<? super E> dst) {
    while (!isEmpty()) {
        dst.add(pop());
    }
}
  • pushAll과 짝을 이루는 popAll 메서드다.

 

입력 매개변수가 생산자와 소비자 역할을 동시에 한다면 와일드카드 타입을 써도 좋을 게 없다.

  • 타입을 정확히 지정해야 하는 상황으로, 이때는 와일드카드 타입을 쓰지 말아야 한다.
  • 다음 공식을 외워두면 어떤 와일드카드 타입을 써야 하는지 기억하는 데 도움이 될 것이다.
    펙스(PECS) : producer-extends, consumer-super
  • PECS 공식은 와일드카드 타입을 사용하는 기본 원칙이다. (겟풋 원칙, Get and Put Principle)

 

반환 타입에는 한정적 와일드카드 타입을 사용하면 안 된다.

  • 유연성을 높여주기는커녕 클라이언트 코드에서도 와일드카드 타입을 써야 하기 때문이다.

 

예시) 아이템30의 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)  // 수정된 버전
  • 입력 매개변수 : E 인스턴스를 생산하므로 원래의 List<E>를 List<? extends E>로 수정했다.
  • 타입 매개변수 : 원래 선언에서는 E가 Comparable<E>를 확장한다고 정의했는데, 이때 Comparable<E>는 E 인스턴스를 소비한다. 그래서 매개변수화 타입 Comparable<E>를 한정적 와일드카드 타입인 Comparable<? super E>로 대체했다.
    • Comparable은 언제나 소비자이므로, 일반적으로 Comparable<E>보다는 Comparable<? super E>를 사용하는 편이 낫다.
    • Comparator도 마찬가지로 Comparator<E>보다는 Comparator<? super E>를 사용하는 편이 낫다.
  • 수정된 버전의 max는 이 책에서 가장 복잡한 메서드 선언일 것이다. 이렇게까지 복잡하게 만들만한 가치가 있을까?
    • 답은 '그렇다'이다.
    • 다음 리스트는 오직 수정된 max로만 처리할 수 있다. List<ScheduledFuture<?>> scheduledFutures = ...;
    • 수정 전 max가 이 리스트를 처리할 수 없는 이유는 java.util.concurrent 패키지의 ScheduledFuture가 Comparable<ScheduledFuture>를 구현하지 않았기 때문이다.
    • ScheduledFuture는 Delayed의 하위 인터페이스이고, Delayed는 Comparable<Delayed>를 확장했다.
    • 다시 말해, ScheduledFuture의 인스턴스는 다른 ScheduledFuture 인스턴스 뿐 아니라 Delayed 인스턴스와도 비교할 수 있어서 수정 전 max가 이 리스트를 거부하는 것이다.
    • 즉, Comparable(혹은 Comparator)을 직접 구현하지 않고, 직접 구현한 다른 타입을 확장한 타입을 지원하기 위해 와일드카드가 필요하다.

 

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

public static <E> void swap(List<E> list, int i, int j);
public static void swap(List<?> list, int i, int j);
  • 주어진 리스트에서 명시한 두 인덱스의 아이템들을 교환(swap)하는 정적 메서드이다.
  • 첫번째는 비한정적 타입 매개변수(아이템30)를 사용했고, 두번째는 비한정적 와일드카드를 사용했다.
  • public api라면 간단한 두번째가 낫다.
  • 기본 원칙은 다음과 같다. 메서드 선언에 타입 매개변수가 한 번만 나오면 와일드카드로 대체하라.
public static void swap(List<?> list, int i, int j) {
    list.set(i, list.set(j, list.get(i)));
}

// 에러 메시지
// list.set(i, list.set(j, list.get(i)));
//                                 ^
  • 하지만 두 번째 swap 선언에는 문제가 하나 있는데, 위와 같이 아주 직관적으로 구현한 코드가 컴파일되지 않는다는 것이다.
  • 방금 꺼낸 원소를 리스트에 다시 넣을 수 없다니, 이게 대체 무슨 일인가?
  • 원인은 리스트의 타입이 List<?>인데, List<?>에는 null 외에는 어떤 값도 넣을 수 없다는 데 있다.
  • 다행히, 형변환이나 리스트의 로 타입을 사용하지 않고 해결하는 방법이 있다.
    • 와일드카드 타입의 실제 타입을 알려주는 메서드를 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 메서드는 리스트가 List<E>임을 알고 있다.

 

결론

swap 메서드 내부에서는 더 복잡한 제네릭 메서드를 이용했지만, 덕분에 외부에서는 와일드카드 기반의 멋진 선언을 유지할 수 있었다.

 

 

 

※ 매개변수(parameter)는 메서드 선언에 정의한 변수이고, 인수(argument)는 메서드 호출 시 넘기는 '실젯값'이다.

    class Set<T> { ... }

    Set<Integer> = ...;

여기서 T는 타입 매개변수가 되고, Integer는 타입 인수가 된다.