아이템 28에서 이야기했듯 매개변수화 타입은 불공변이다. 즉, 서로 다른 타입 Type1
과 Type2
가 있을 때 List<Type1>
은 List<Type2>
의 하위 타입도 상위 타입도 아니다.
하지만 때론 불공변 방식보다 유연한 방식이 필요하다. 아이템 29의 Stack
클래스를 예로 설명하겠다.
Stack
의 public API
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);
}
이 메서드는 컴파일 잘 되지만 완벽하지 않다. src
의 원소 타입이 스택의 원소 타입과 일치하면 잘 작동한다. 하지만 Stack<Number>
로 선언한 후 Integer
타입인 intVal
로 pushAll(intVal)
을 호출하면 오류 메시지가 발생한다. 매개변수화 타입이 불공변이기 때문에 Integer
가 Number
의 하위 타입이라도 오류가 발생한 것이다.
자바는 이런 상황에서 한정적 와일드카드 타입이라는 특별한 매개변수화 타입을 지원한다. pushAll
의 입력 매개변수 타입은 'E
의 Iterable
'이 아니라 'E
의 하위 타입의 Iterable
'이어야 하며, 와일드카드 타입 Iterable<? extends 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>
의 원소를 Object
용 컬렉션으로 옮기려하면 오류가 발생한다. 아까와 마찬가지로 매개변수화 타입은 불공변이기 때문이다. 이번에도 와일드카드 타입으로 해결할 수 있다. popAll
의 입력 매개변수의 타입이 'E
의 Collection
'이 아니라 'E
의 상위 타입의 Collection
'이어야 한다. Collection<? super E>
가 정확이 이런 뜻이다.
public void popAll(Collection<? super E> dst){
while(!isEmpty())
dst.add(pop());
}
위와 같이 수정하면 타입 안전하게 컴파일 잘 된다.
와일드카드 타입이 주는 메시지는 분명하다. 유연성을 극대화하려면 원소의 생산자나 소비자용 입력 매개변수에 와일드카드 타입을 사용하라. 한편, 입력 매개변수가 생산자와 소비자 역할을 동시에 한다면 와일드카드 타입을 써도 좋을 게 없다. 타입을 정확히 지정해야 하는 상황으로, 이때는 와일드카드 타입을 쓰지 말아야 한다.
다음 공식을 외워두면 어떤 와일드카드 타입을 써야하는지 기억하는 데 도움이 된다.
펙스(PECS): producer-extends, consumber-super
즉, 매개변수화 타입 T
가 생상자라면 <? extend T>
를 사용하고, 소비자라면 <? super T>
를 사용하라는 것이다. Stack
의 예에서 pushAll
의 src
매개변수는 Stack
이 사용할 E
인스턴스를 생산하므로 src
은 Iterable<? extends E>
타입으로 정의되었다. 한편, popAll
의 dst
매개변수는 Stack
으로부터 E
인스턴스를 소비하므로 dst
의 적절한 타입은 Collection<? super E>
이다.
반환 타입에는 한정적 와일드카드 타입을 사용하면 안 된다. 유연성을 높여주기는커녕 클라이언트 코드에서도 와일드카드 타입을 써야 하기 때문이다.
클래스 사용자가 와일드카드 타입을 신경 써야 한다면 그 API에 무슨 문제가 있을 가능성이 크다.
앞의 코드는 자바 8부터 제대로 컴파일 된다. 자바 7까지는 타입 추론 능력이 충분히 강력하지 못해서 문맥에 맞는 반환 타입을 명시해야 했다. 컴파일러가 올바른 타입을 추론하지 못할 때면 언제든 아래와 같이 명시적 타입 인수를 사용해서 타입을 알려주면 된다.
Set<Number> numbers=Union.<Number>union(integers, doubles);
max
메서드를 와일드카드 타입을 사용한 코드로 변경했다.
public static<E extends Comparalbe<E>> E max(List<E> list)
public static<E extend Comparable<? super E>> E max(List<? extend E> list)
Comparable
은 언제나 소비자이므로, 일반적으로 Comparable<E>
보다는 Comparable<? super E>
를 사용하는 편이 낫다. Comparator
도 마찬가지이다. 그러면 Comparable
을 직접 구현하지 않고, 직접 구현한 다른 타입을 확장한 타입을 지원해준다.
메서드를 정의할 때 타입 매개변수와 와일드카드 중 어떤 선언이 더 나을까? public API
라면 와일드카드가 낫다. 기본 규칙은 메서드 선언에 타입 매개변수가 한 번만 나오면 와일드카드로 대체해라. 라는 것이다. List<?>
에는 null
외에는 어떤 캆도 넣을 수 없다. 만약에 값을 넣고 싶으면 와일드카드 타입의 실제 타입을 알려주는 메서드를 private
도우미 메서드로 작성하여 해결하면 된다.