아이템 31. 한정적 와일드카드를 사용해 API 유연성을 높이라. - ksw6169/effective-java GitHub Wiki
- 매개변수화 타입은 불공변으로 서로 간에 상위 타입도 아니고 하위 타입도 아니다.
- 예를 들어
List<Object>
에는 어떤 객체든 넣을 수 있지만,List<String>
에는 문자열만 넣을 수 있다. - 이는 상위 타입의 객체를 하위 타입의 객체로 치환해도 프로그램은 정상 동작해야 한다는 리스코프 치환 원칙에 위배되는 것이므로
List<String>
은List<Object>
의 하위 타입이 아니다.
- 하지만 때로는 매개변수화 타입을 사용할 때 상위 타입의 객체를 받거나 하위 타입의 객체를 받아 유연하게 처리해야 하는 상황이 있을 수 있다.
- 이 때는 매개변수화 타입에 한정적 와일드카드를 사용하여 코드에 유연성을 부여할 수 있다.
아래는 Stack 클래스이다.
public class Stack<E> {
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);
}
이를 사용하는 코드는 다음과 같을 수 있다.
Stack<Number> numberStack = new Stack<>();
Iterable<Integer> integers = ...;
numberStack.pushAll(integers);
Integer
는 Number
의 하위 타입이니 잘 동작해야 할 것 같지만 실제로는 오류 메시지가 발생한다. 매개변수화 타입이 불공변이기 때문이다.
StackTest.java:7: error: incompatible types: Iterable<Integer>
cannot be converted to Iterable<Number>
numberStack.pushAll(integers);
^
이 때는 특별한 매개변수화 타입인 한정적 와일드카드 타입을 사용해 대처할 수 있다.
// E의 하위 타입을 받아 처리할 수 있도록 한정적 와일드카드 타입을 적용함
public void pushAll(Iterable<? extends E> src) {
for (E e : src)
push(e);
}
마찬가지로 아래와 같이 popAll()
이라는 메소드가 있다면
public void popAll(Collection<E> dst) {
while (!isEmpty())
dst.add(pop());
}
이를 사용하는 코드는 다음과 같을 수 있다.
Stack<Number> numberStack = new Stack<>();
Collection<Object> objects = ...;
numberStack.popAll(objects);
하지만 pushAll()
때와 마찬가지로 컴파일을 하면 에러가 발생한다. 이를 해결하기 위해 한정적 와일드카드 타입을 적용하면 다음과 같다.
public void popAll(Collection<? super E> dst) {
while (!isEmpty())
dst.add(pop());
}
와일드카드 타입을 쓰지 말아야 하는 상황
- 입력 매개변수가 생산자와 소비자 역할을 동시에 한다면 와일드카드 타입을 써도 좋을 게 없다.
- 이 때는 타입을 정확히 지정해야 하는 상황으로 와일드카드 타입을 쓰지 말아야 한다.
- PECS 공식은 와일드카드 타입을 사용하는 기본 원칙이다.
- PECS라는 공식을 외워두면 어떤 와일드카드 타입을 써야 하는지 기억하는 데 도움이 될 것이다.
- PECS는 매개변수화 타입
T
가 생산자라면<? extends T>
를 사용하고, 소비자라면<? super T>
를 사용하라는 공식이다.
choices 컬렉션은 T 타입의 값을 생산하기만 한다.
public Chooser(Collection<T> choices)
이를 PECS 공식에 따라 개선하면 다음과 같다.
public Chooser(Collection<? extends T> choices)
s1과 s2 모두 E의 생산자다.
public static <E> Set<E> union(Set<E> s1, Set<E> s2)
이를 PECS 공식에 따라 개선하면 다음과 같다.
public static <E> Set<E> union(Set<? extends E> s1, Set<? extends E> s2)
수정한 메소드를 사용하는 클라이언트 코드는 다음과 같다.
Set<Integer> integers = Set.of(1,3,5);
Set<Double> doubles = Set.of(2.0, 4.0, 6.0);
Set<Number> numbers = union(integers, doubles);
주의 사항
반환 타입에는 한정적 와일드카드 타입을 사용하면 안된다. 유연성을 높여주기는 커녕 클라이언트 코드에서도 와일드카드 타입을 써야 하기 때문이다.
명시적 타입 인수(explicit type argument)
- Java 7까지는 타입 추론 능력이 충분히 강력하지 못하다. 따라서 명시적 타입 인수를 추가해줘야 하는 경우가 있을 수 있다.
Set<Number> numbers = Union.<Number> union(integers, doubles);
타입 매개변수와 타입 인수
매개변수는 메소드 선언에 정의한 변수이고, 인수는 메소드 호출 시 넘기는 실젯값이다. 이는 제네릭에서도 동일하게 적용되어
Set<T>
에서는T
를 타입 매개변수라 하며, 이를 사용하는Set<Integer>
에서Integer
는 타입 인수라 한다.
- Effective Java 3/E