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);
Integer
는Number
의 하위 타입이기 때문에 잘 동작할 것 같지만 오류가 뜬다.- 매개 변수화 타입이 불공변이기 때문이다.
[해결책] 한정적 와일드카드 타입(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.