Post

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

매개변수화 타입은 불공변이다.

  • List<Object>List<String>의 하위 타입도 상위 타입도 아니다.
  • List<String>List<Object>가 하는 일을 제대로 수행하지 못한다.
    • 리스코프 치환법칙에 어긋난다. (아이템 10)

때로는 불공변 방식보다 유연한 무언가가 필요하다.

아이템 29의 Stack 클래스

1
2
3
4
5
6
public class Stack<E> {
    public Stack();
    public void push(E e);
    public E pop();
    public boolean isEmpty();
}

pushAll 메서드

와일드 카드 타입을 사용하지 않은 pushAll 메서드

1
2
3
4
public void pushAll(Iterable<E> src) {
    for (E e : src)
        push(e);
}
  • 위 메서드는 결함이 있다. 😨
  • Iterable src의 원소 타입이 스택의 원소 타입과 일치하면 잘 작동한다.

하지만, Stack<Number> 선언 후, Integer 타입의 src를 넣으면 어떻게 될까?

1
2
3
4
Stack<Number> numberStack = new Stack<>();

Iterable<Integer> integers = Arrays.asList(3, 1, 4, 1, 5, 9);
numberStack.pushAll(integers);
  • IntegerNumber의 하위 타입이기 때문에 잘 동작할 것 같지만 오류가 뜬다.
  • 매개 변수화 타입이 불공변이기 때문이다.

[해결책] 한정적 와일드카드 타입(extends)

와일드카드 타입을 사용하도록 pushAll 메서드를 수정했다.

pushAll의 입력 매개변수 타입은 E의 Iterable이 아니라 E의 하위 타입의 Iterable이어야 한다.

1
2
3
4
5
// 생산자(producer) 매개변수에 와일드카드 타입 적용
public void pushAll(Iterable<? extends E> src) {
    for (E e : src)
        push(e);
}
  • 클라이언트 코드도 말끔히 컴파일된다. (타입 안전)

popAll 메서드

와일드 카드 타입을 사용하지 않은 popAll 메서드

1
2
3
4
public void popAll(Collection<E> dst) {
	while(!isEmpty())
		dst.add(pop());
}
  • 위 메서드도 완벽하진 않다. 🙃
  • 주어진 컬렉션의 원소 타입이 스택의 원소 타입과 일치한다면 문제 없이 동작한다.

하지만, Stack<Number>의 원소를 Object용 컬렉션으로 옮기려 한다면 어떻게 될까?

1
2
3
4
Stack<Number> numberStack = new Stack<>();

Collection<Object> objects = new ArrayList<>();
numberStack.popAll(objects); // 오류 발생
  • Collection<Object>Collection<Number>의 하위 타입이 아니다’라는 오류가 발생한다.

[해결책] 한정적 와일드카드 타입(super)

Stack안의 모든 원소를 주어진 컬렉션으로 옮겨 담는다.

E의 상위 타입의 Collection이어야 한다. 즉, Collection<? super E> 이렇게 사용해야 한다.

1
2
3
4
5
// 와일드카드 타입 적용
public void popAll(Collection<? super E> dst) {
    while(!isEmpty())
        dst.add(pop());
}

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

PECS 공식 : T가 생산자면 <? extends T>, T가 소비자면 <? super T>

제대로만 사용하면 클라이언트는 와일드카드 타입이 쓰였다는 사실조차 의식하지 못한다.

클라이언트가 와일드카드 타입을 신경 써야 한다면 API에 문제가 있을 가능성이 크다.

자바 7 이하에서는 위의 변경이 소용이 없다.

명시적 타입 인수를 사용해서 타입을 알려줘야 한다.

1
2
3
4
5
Set<Integer> integers = Set.of(1, 3, 5);
Set<Double> doubles = Set.of(2.0, 4.0, 6.0);

// 명시적 타입인수
Set<Number> numbers = Union.<Number>(integers, doubles);

Comparable과 Comparator

Comparable과 Comparator는 소비자이기 <E>보다는 <? super E>를 사용하는 것이 낫다.

와일드카드 활용 팁

  • 메서드 선언에 타입 매개변수가 한 번만 나오면 와일드카드로 대체하라.
    • 한정적 타입이라면 한정적 와일드카드로
    • 비한정적 타입이라면 비한정적 와일드카드로
  • 주의 🤔
    • 비한정적 와일드카드로 정의한 타입에는 null을 제외한 아무것도 넣을 수 없다.
1
2
3
4
5
6
7
8
9
10
11
12
public static void swap(List<?> list, int i, int j); // 컴파일 오류발생..

// ---------- 해결법 ------------
// 실제 타입을 알려줄 도우미 메서드를 사용한다.
public static void swap(List<?> list, int i, int j) {
    swapHelper(list, i, j);
}

// 도우미 메서드가 리스트의 타입이 항상 E인 것을 알기 때문에 안전함을 알고 있다.
private static <E> void swapHelper(List<E> list, int i, int j) {
    list.set(i, list.set(j, list.get(i)));
}

💡 핵심 정리

  • 조금 복잡하더라도 와일드카드 타입을 적용하면 api가 훨씬 유연해진다.
  • PECS 공식을 기억하자. (생산자는 extends, 소비자는 super 사용)

참고 🕶️

타입 추론

  • Type Inference (The Java™ Tutorials > Learning the Java Language > Generics)
  • 타입을 추론하는 컴파일러의 기능
  • 모든 인자의 가장 구체적인 공통 타입 (most specific type)
  • 제네릭 메서드와 타입 추론: 메서드 매개변수를 기반으로 타입 매개변수를 추론할 수 있다.
  • 제네릭 클래스 생성자를 호출할 때 다이아몬드 연산자 <>를 사용하면 타입을 추론한다.
  • 자바 컴파일러는 “타겟 타입”을 기반으로 호출하는 제네릭 메서드의 타입 매개변수를 추론한다.
    • 자바 8에서 “타겟 타입”이 “메서드의 인자”까지 확장되면서 타입 추론이 강화되었다.
This post is licensed under CC BY 4.0 by the author.