이펙티브 자바 - item 31. 한정적 와일드카드를 사용해 API 유연성을 높이라

item 31. 한정적 와일드카드를 사용해 API 유연성을 높이라

이펙티브 자바 - item 31. 한정적 와일드카드를 사용해 API 유연성을 높이라

로 타입으로 작성된 두 집합의 합집합을 반환하는 메서드

클래스와 마찬가지로 메서드도 제너릭으로 만들 수 있다.

와일드카드 타입을 사용하지 않는 스택

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

public void popAll(Collection<E> dst){
    while (!isEmpty())
        dst.add(pop());
}
public static void main(String[] args) {
  //pushAll
  StackV1<Number> numberStackV1 = new StackV1<>();
  List<Number> numVal = List.of(1, 2, 3.4, 4L);
  List<Integer> intVal = List.of(5, 6);
	// 정상 동작
  numberStackV1.pushAll(numVal);
  // incompatible types: java.util.List<java.lang.Integer> cannot beconverted to java.lang.Iterable<java.lang.Number>
	numberStackV1.pushAll(intVal);

  //popAll
	Collection<Object> objects = new ArrayList<>();
  //java: incompatible types: java.util.Collection<java.lang.Object>cannot be converted to java.util.Collection<java.lang.Number>
  numberStackV1.popAll(objects);
}

위와 같은 와일드카드 타입을 사용하지 않는 메서드를 실행시키려고 하면 위와 같은 에러가 발생한다.

이는 불공변인 제네릭을 매개변수 타입으로 사용했기 때문이다. 이럴때 한정적 와일드카드타입이라는 특별한 매개변수화 타입을 이용하면 이를 해결할 수 있다.

한정적 와일드카드 타입을 사용한 스택

public void pushAll(Iterable<? extends E> src) { // 한정적 와일드카드 적용
    for (E e : src) {
        push(e);
    }
}

public void popAll(Collection<? super E> dst){ // 한정적 와일드카드 적용
    while (!isEmpty())
        dst.add(pop());
}

pushAll의 입력 매개변수 타입은 ‘E의 Iterable 이 아니라 ‘E의 하위 타입위 Iterable’이어야 하며, 와일드카드 타입 Iterable<? extends E>가 정확히 이런 뜻이다.

popAll의 입력 매개변수 타입은 ‘E의 Collection’이 아니라 ‘E의 상위 타입의 Collection’이어야 한다. 이를 와일드카드 타입을 사용하면 Collection<? super E>로 표현할 수 있다.

이와 같이 유연성을 극대화하려면 원소의 생산자나 소비자용 입력 매개변수에 와일드카드 타입을 사용할 수 있다. 하지만 입력 매개변수가 생산자와 소비자 역할을 동시에 한다면 와일드카드 타입을 사용하면 안된다.

어떤 와일드카드 타입을 사용해야하는가?

팩스(PECS) 공식에 따라 어떤 와일드카드 타입을 써야하는지 결정할 수 있다.

매개변수화 타입 T가 생산자라면 <? extends T>를 사용한다.

배개변수화 타입 T가 소비자라면 <? super T를 사용한다.

PECS 공식에 따라 item-30의 union 함수를 수정하면 아래와 같다.

// 반환 타입은 여전히 Set<E>임에 주목
// 반환 타입에는 한정적 와일드카드 타입을 사용하면 안 된다.
// 유연성을 높여주기는 커녕 클라이언트 코드에서도 와일드카드 타입을 써야하기 때문
public static <E> Set<E> union(Set<? extends E> s1, Set<? extends E> s2) { //s1, s2 둘 다 생산자이므로 <? extends>를 사용
    Set<E> result = new HashSet<>(s1);
    result.addAll(s2);
    return result;
}

public static void main(String[] args) {
        Set<Integer> integers = Set.of(1, 3, 5);
        Set<Double> doubles = Set.of(2.0, 4.0, 6.0);
        Set<Number> union = union(integers, doubles);
    }

받아들여야 할 매개변수를 받고 거절해야 할 매겨변수는 거절하는 작업이 알아서 이뤄진다. 클래스 사용자가 와일드카드 타입을 신경 써야 한다면 그 API에 무슨 문제가 있을 가능성이 크다.

한정적 와일드카드 타입의 복잡한 사용법

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를 List<? extends E>로 수정한다.

  • Comparale은 언제나 소비자이므로, 일반적으로 Comparable<? super E>를 사용한다.

  • 마찬가지로 Comparator 또한 일반적으로 Comparator<? super E>를 사용한다.

Comparable(혹은 Comparator)을 직접 구현하지 않고, 직접 구현한 다른 타입을 확장한 타입을 지원하기 위해서는 위와 같이 와일드카드가 필요하다.

수정전 max() 메서드는 List<ScheduledFuture<?» scheduledFutures = …;를 처리할 수 없다. ScheduledFuture는 Delayed의 하위 인터페이스이고, Delayed는 Comparable를 확장했다. 즉, ScheduledFuture의 인스턴스는 다른 ShceduledFuture 인스턴스뿐 아니라 Delayed 인스턴스와도 비교할 수 있기 때문에 수정 전 max가 List<ScheduledFuture<?»를 거부한다. 아래의 상속 관계와 함께 생각해보자.

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

메서드 정의 시 비한정적 타입 매개변수와 비한정적 와일드 카드 중 어떤 방법이 좋을까?

class Example {
    public static <E> void swap(List<E> list, int i, int j); // 비한정적 타입 매개변수 사용

    public static void swap(List<?> list, int i, int j); // 비한정적 와일드 카드 사용
}

public API를 정의하는 경우 두 번째 방법을 사용하는 것이 좋다. 어떤 리스트든 이 메서드에 넘기면 명시한 인덱스의 원소들을 교환해 줌과 동시에 신경 써야 할 타입 매개변수도 없다.

  • 기본 규칙
    • 메서드 선언에 타입 매개변수가 한 번만 나오면 와일드 카드로 대체하라
    • 이때 비한정적 타입 매개변수인 경우 비한정적 와일드카드로 바꾸고, 한정적 타입 매개변수라면 한정적 와일드카드로 바꾸면 된다.

두 번째 방법을 사용하는 경우 주의사항

List<?>에는 null 외에 어떤 값도 넣을 수 없어 방금 꺼낸 원소를 리스트에 다시 넣을 수 없는 오류를 발생시키면서 컴파일 되지 않는다.

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

위 문제를 형변환이나 리스트의 로 타입을 사용하지 않고 해결하기 위해서는 와일드 카드 타입의 실제 타입을 알려주는 메서드를 private 도우미 메서드로 따로 작성하여 사용해야한다.

실제 타입을 알아내려면 이 도우미 메서드는 제네릭 메서드여야 한다.

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

 // 와일드카드 타입을 실제 타입으로 바꿔주는 private 도우미 메서드
public static <E> void swapHelper(List<E> list, int i, int j) {
  list.set(i, list.set(j, list.get(i)));
 }

swapHelper 메서드는 리스트가 List임을 알고 있다.

즉, 이 리스트에서 꺼낸 값의 타입은 항상 E이고, E 타입의 값이라면 이 리스트에 넣어도 안전함을 알고있다.

swap 메서드를 호출하는 클라이언트는 복잡한 swapHelper의 존재를 모른 채 그 혜택을 누리는 것이다.

PECS 공식

PECS는 Java 제네릭스(Generics)에서 사용되는 약어로, Producer(생산자)와 Consumer(소비자)의 역할을 설명하는데 사용됩니다. 이는 Java의 제네릭 타입에서 타입 매개변수를 사용할 때 자주 등장하는 개념입니다.

PECS란?

P - Producer의 역할을 나타내는 타입 매개변수를 의미합니다. 이는 데이터를 > 생성하거나 반환하는 메서드에 사용되는 타입입니다.

E - Element(요소)의 약자로, 컬렉션의 요소 타입을 나타냅니다.

C - Consumer의 역할을 나타내는 타입 매개변수를 의미합니다. 이는 데이터를 > 소비하는 메서드에 사용되는 타입입니다.

S - Super의 역할을 나타내는 타입 매개변수를 의미합니다. 이는 데이터를 > 저장하거나 받아들이는 메서드에 사용되는 타입입니다.

PECS 원칙

  • Producer extends Consumer super: 이 원칙은 “생산자는 확장(extends)되고, 소비자는 super로 제한”됩니다. 즉, 메서드가 데이터를 생성(생산)할 때는 Producer로 제한하고, 메서드가 데이터를 사용(소비)할 때는 Consumer로 제한합니다.
  • Producer는 데이터를 생성하기 때문에 Producer가 소비할 수 있는 데이터 타입보다 더 구체적인 타입으로 제한됩니다.
  • Consumer는 데이터를 사용하기 때문에 Consumer가 생산할 수 있는 데이터 타입보다 더 일반적인 타입으로 제한됩니다.

핵심 정리

  • 조금 복잡하더라도 와일드 카드 타입을 적용하면 API가 훨씬 유연해진다.
  • 널리 쓰일 라이브러리를 작성한다면 반드시 와일드카드 타입을 적절히 사용해야 한다.
  • 생산자(producer)는 extends를 소비자(consumer)는 super를 사용한다.
  • Comparable과 Comparator는 모두 소비자라는 사실을 잊지 않아야 한다.