매개변수화 타입은 불공변임 서로 다른 타입 Type1
과 Type2
가 있을 때 List<Type1>
은 List<Type2>
의 하위 타입도 상위 타입도 아님
즉 List<String>
은 List<Object>
의 하위 타입이 아니라는 뜻임
따지고 보면 List<Object>
는 어떤 객체든 넣을 수 있지만 List<String>
에는 문자열만 넣을 수 있음 List<String>
은 List<Object>
가 하는 일을 제대로 수행하지 못해 하위 타입이 될 수 없음
하지만 때론 불공변 방식보다 유연한 무언가가 필요함
예를 들어 일련의 원소를 스택에 넣는 메서드가 있을 때
public void pushAll(Iterable<E> src) {
for (E e : src)
push(e);
}
위와 같이 쓴다면 컴파일이 되도 완벽하진 않음, Iterable src
의 원소 타입이 스택의 원소 타입과 일치하면 잘 작동하지만 타입이 다르면 오류 메시지가 뜸
Number
타입 대신 Integer
가 들어가도 오류가 뜸 하위타입이어도 그 이유는 매개변수화 타입이 불공변이기 때문임
이런 상황에 대처하기 위해서 한정적 와일드카드 타입이라는 특별한 매개변수화 타입을 지원함
public void pushAll(Iterable<? extends E> src) {
for (E e : src)
push(e);
}
위와 같이 쓴다면 매개변수 타입은 E의 Iterable
이 아니라 E의 하위 타입의 Iterable
이어야 하며, 와일드카드 타입 Iterable<? extends E>
가 정확히 이런 뜻임(이 extends
가 확장한 것을 의미한 것은 아님)
위처럼 적용하게 된다면 컴파일이 말끔히 됨
여기서 popAll
메서드 역시 Collection<Object>
와 Collection<Number>
가 불공변이므로 하위 타입이 아니기 때문에 이 역시 와일드카드 타입을 적용해야함
public void popAll(Collection<? super E> dst) {
while (!isEmpty())
dst.add(pop());
}
위와 같이 쓰면 입력 매개변수의 타입이 E의 Collection
이 아니라 E의 상위 타입의 Collection
이어야 한다는 의미가 됨
여기서 전달하는 메시지는 분명함, 유연성을 극대화하려면 원소의 생산자나 소비자용 입력 매개변수에 와일드카드 타입을 사용하는 것임
하지만 입력 매개변수가 생산자와 소비자 역할을 동시에 한다면 와일드카드 타입을 써도 좋을게 없음, 타입을 정확히 지정해야 하는 상황이므로
펙스(PECS) : producer-extends, consumer-super
매개변수화 타입 T
가 생산자라면 <? extends T>
를 사용하고 소비자라면 <? super T>
를 사용함
이 PECS 공식은 와일드카드 타입을 사용하는 기본 원칙임
앞서 아이템에서 본 메서드와 생성자 선언 역시 수정할 수 있음
public Chooser(Collection<T> choices)
choices
컬렉션은 T
타입의 값을 생산하기만 하니 T
를 확장하는 와일드카드 타입을 사용해 선언해야함public Chooser(Collection<? extends T> choices)
union
메서드 역시 수정 가능함 모두 E
의 생성자이므로 아래와 같이 씀public static <E> set<E> union(Set<? extends E> s1, Set<? extends E> s2)
이런식으로 제대로만 사용하면 클래스 사용자는 와일드카드 타입이 쓰였다는 사실조차 의식하지 못함, 받아들여야 할 매개변수를 받고 거절해야 할 매개변수는 거절하는 작업이 알아서 이뤄짐
하지만 위의 코드는 자바 8부터 제대로 컴파일 됨 자바 7까지는 타입 추론 능력이 충분히 강력하지 못해서 문맥에 맞는 반환 타입(혹은 목표 타입)을 명시해야함
아래와 같이 자바 7에서는 명시적 타입 인수를 사용해서 목표 타입을 알려줘야함
Set<Number> numbers = Union.<Number>union(integers, doubles);
max
메서드를 보면 원래 버전은 아래와 같음public static <E extends Comparable<E>> E max(List<E> list)
public static <E extends Comparable<? super E>> E max(List<? extends E> list)
PECS 공식을 두 번 적용함, 입력 매개변수에서는 E 인스턴스를 생산하므로 List<? extends E>
로 수정함
그 다음 타입 매개변수를 보면 원래 선언에선 E
가 Comparable<E>
를 확장한다고 정의했는데 이때 Comparable<E>
는 E
인스턴스를 소비함, 그래서 한정적 와일드카드 타입인 Comparable<? super E>
로 대체함
Comparable
은 언제나 소비자이므로, 일반적으로 Comparable<E>
보다는 Comparable<? super E>
를 사용하는 편이 나음
Comparator
역시 마찬가지임
이렇게 하는 이유는 위와 같이 수정된 메서드에서만 List<ScheduledFuture<?>> scheduledFutures = ...;
를 쓸 수 있음
이는 Comparable
을 직접 구현하지 않고 직접 구현한 다른 타입을 확장한 타입을 지원하기 위해 와일드카드가 필요하기 때문임
무슨말이냐면 ScheduledFuture
자체가 Delayed
의 하위 인터페이스이고 Delayed
는 Comparable<Delayed>
를 확장했기 때문에 수정전 max
는 불공변이므로 이를 타입이나 하위 관계로써 생각하지 않기 때문에 와일드카드를 써야하는 것임
하나 더 논의할 주제가 있는데 타입 매개변수와 와일드카드에는 공통된 부분이 있어서, 메서드를 정의할 때 둘 중 어느 것을 사용해도 괜찮을 때가 많음
public static <E> void swap(List<E> list, int i, int j); // 비한정적 타입 매개변수
public static void swap(List<?> list, int i, int j); // 비한정적 와일드카드
위의 swap의 경우 두 번째가 나음, 어떤 리스트든 이 메서드에 넘기면 명시한 인덱스의 원소들을 교환해 줄 것임, 신경 써야 할 타입 매개변수도 없음
기본 규칙은 메서드 선언에 타입 매개변수가 한 번만 나오면 와일드카드로 대체하라임
이 때 비한정적 타입 매개변수라면 비한정적 와일드카드로 바꾸고, 한정적 타입 매개변수라면 한정적 와일드카드로 바꾸면 됨
하지만 여기서 무작정 아래와 같이 직관적으로 쓰면 컴파일 되지 않음
public static void swap(List<?> list, int i, int j) {
list.set(i, list.set(j, list.get(i)));
}
리스트의 타입이 List<?>
인데 List<?>
에는 null
외에는 어떤 값도 넣을 수 없다는 데 있음
여기서 로 타입을 사용하지 않고 바로 와일드카드 타입의 실제 타입을 알려주는 메서드를 private
도우미 메서드로 따로 작성하여 활용할 수 있음
public static void swap(List<?> list, int i, int j) {
swapHelper(list, i, j);
}
// 와일드카드 타입을 실제 타입으로 바꿔주는 private 도우미 메서드
private static <E> void swapHelper(List<E> list, int i, int j) {
list.set(i, list.set(j, list.get(i)));
}
swapHelper
메서드는 리스트가 List<E>
임을 알고 있음, 이 리스트에서 꺼낸 값의 타입은 항상 E
이고, E
타입의 값이라면 이 리스트에 넣어도 안전함을 알고 있음
swap
메서드 내부에서는 더 복잡한 제네릭 메서드를 이용했지만, 덕분에 외부에서는 와일드카드 기반의 멋진 선언을 유지할 수 있었음
swap
메서드를 호출하는 클라이언트는 복잡한 swapHelper
의 존재를 모른 채 그 혜택을 누림