💡 유연성을 극대화하려면 원소의 생산자나 소비자용 입력 매개변수에 와일드카드 타입을 사용하라.
한편, 입력 매개변수가 생산자와 소비자 역할을 동시에 한다면 타입을 정확히 지정해주어야 하므로 와일드 카드 타입을 쓰지 말아야 한다.
✒️ 펙스(PECS) 공식 : producer-extends, consumer-super
1. 변성 (variance)
1. 공변성 (covariant) : A가 B의 하위타입일 때, List<A>가 List<B>의 하위타입인 경우
2. 반공변성 (contravariant) : A가 B의 상위타입일 때, List<A>가 List<B>의 상위타입인 경우
3. 무변성 (invariant) : A가 B의 타입이지만 List<A>와 List<B>가 아무 관계가 없는 경우
자바의 제네릭, 즉 매개변수화 타입(Item 28)은 불공변(invariant)을 따른다.
예를 들어, List<String>은 List<Object>가 하는 일을 제대로 수행하지 못하니 리스코프 치환 원칙에 어긋나므로 하위 타입이 될 수 없다.
하지만 때론 불공변 방식보단 유연한 무언가가 필요하다.
2. 한정적 와일드 카드
📌 Ex 1. Stack 클래스
Item 29의 Stack 클래스에서 public API들
package Chat5.Item31;
import java.util.*;
public class Stack<E> {
public Stack();
public void push(E e);
public E pop();
public boolean isEmpty();
}
전체 코드
package Chat5.Item31;
import java.util.*;
public class Stack<E> {
private E[] elements;
private int size = 0;
private static final int DEFAULT_INITIAL_CAPACITY = 16;
@SuppressWarnings("unchecked")
public Stack() {
elements = (E[]) new Object[DEFAULT_INITIAL_CAPACITY];
}
public void push(E e) {
ensureCapacity();
elements[size++] = e;
}
public E pop() {
if (size == 0)
throw new EmptyStackException();
E result = elements[--size];
elements[size] = null; // 다 쓴 참조 해제
return result;
}
public boolean isEmpty() {
return size == 0;
}
}
제네릭 E 타입의 값들을 관리하는 Stack에 컬렉션을 받아 stack에 추가하는 메서드를 정의해보자.
public void pushAll(Iterable<E> src) { // producer
for (E e : src)
push(e);
}
컴파일은 되지만 완벽하지는 않다. Iterable src의 원소타입 Stack의 원소타입이 일치하지 않으면 문제가 발생한다.
Integer 타입의 원소를 가진 Iterable을 Stack<Number>에 넣는 상황을 가정해보자.
논리적으로는 옳으나, 매개변수화 타입이 불공변이기 때문에 문제가 발생한다.
이런 상황에 대처할 수 있는 한정적 와일드 카드 타입이라는 특수 매개변수화 타입을 제공한다.
public void pushAll(Iterable<? extends E> src) { // producer
for (E e : src)
push(e);
}
E는 Stack의 제네릭 타입에 해당한다. <? extends E>는 E를 포함한 하위 타입이 ?에 해당될 수 있다는 뜻이 된다.
이렇게 수정하면 Stack은 물론 클라이언트 코드도 말끔하게 컴파일이 된다. (type-safe을 의미)
pushAll에 대응하는 popAll 메서드도 추가해보자.
public void popAll(Collection<E> dst) { // consumer
while (!isEmpty())
dst.add(pop());
}
이번에도 마찬가지로 원소 타입이 일치하면 문제 없지만, 완벽하지는 않다.
Stack<Number>의 원소를 Object용 컬렉션으로 옮기려 한다고 해보자.
매개변수 타입이 Collection<Number>이므로 Collection<Object>는 컴파일단에서 막힌다.
public void popAll(Collection<? super E> dst) { // consumer
while (!isEmpty())
dst.add(pop());
}
이 경우에는 <? super E>, 즉 E를 포함한 상위 타입이 ?에 해당될 수 있음을 명시해줄 수 있다.
위의 내용이 바로 가장 처음 명시했던 유연성을 극대화하기 위해 매개변수에 와일드 타입을 사용하는 의도이다.
생산자와 소비자 역할을 동시에 한다면 와일드 카드가 아니라 타입을 명시해라.
- 생산자(producer) : 해당 클래스가 사용할 인스턴스를 생산하는 경우 → <? extense T>
- 소비자(consumer) : 해당 클래스의 인스턴스를 소비하는 경우 → <? super T>
PECS 공식을 명심하자.
📌 Ex 2. Chooser 클래스
Item 28의 Chooser 클래스를 가져와보자.
public class Chooser<T> {
...
public Chooser(Collection<T> choices) {
choiceList = new ArrayList<>(choices);
}
...
}
이 생성자로 넘겨지는 choices 컬렉션도 T 타입의 값을 생산하는데만 사용된다.
public Chooser(Collection<? extends T> choices)
이 방법은 실질적인 차이를 가져온다.
예를 들어, Chooser<Number> 생성자에 List<Integer>를 넘기고 싶을 때, 수정 전은 컴파일조차 되지 않는다.
하지만 한정적 와일드 카드 타입으로 선언한 수정본은 생성자에서의 문제가 사라진다.
📌 union 메서드
public static <E> Set<E> union(Set<E> s1, Set<E> s2);
s1, s2 모두 E의 생산자에 해당하므로 PECS 공식에 따라 다음과 같이 선언해야 한다.
public static <E> Set<E> union(Set<? extends E> s1, Set<? extends E> s2);
반환 타입은 여전히 Set<E>임에 주목하자. 반환 타입에는 한정적 와일드 카드를 사용해선 안 된다.
유연성을 높여주긴 커녕 클라이언트 코드에서도 와일드 카드 타입을 써야하게 된다.
수정한 선언을 사용하면 다음 코드도 정상적으로 컴파일된다.
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);
클래스 사용자가 와일드카드 타입을 신경써야 한다면 그 API에 무슨 문제가 있을 가능성이 크다.
📌 max 메서드
(여기 이해하는데 한참 걸렸다.)
public static <E extends Comparable<E>> E max(List<E> list)
우선 제네릭 메서드의 메서드 목록 타입은 클래스와 별개인 지역 변수 느낌으로 취급해야 함을 상기하자.
Class의 E 타입이 Integer라고 해서 제네릭 메서드의 E와 동일하지는 않다.
이를 유의하여 와일드 카드 타입으로 다듬으면 아래와 같다. (PECS 공식이 두 번이나 적용된다.)
public static <E extends Comparable<? super E>> E max(List<? extends E> list)
public static <E extends Comparable<? super E>> E max(List<? extends E> c) {
if (c.isEmpty()) {
throw new IllegalArgumentException("collection is empty");
}
E result = null;
for (E e : c) {
if (result == null || e.compareTo(result) > 0) {
result = Objects.requireNonNull(e);
}
}
return result;
}
입력 매개변수에서는 E 인스턴스를 생산하므로 List<? extends E>로 수정했다.
해당 메서드에서 Comparable<E>은 E 인스턴스를 소비한다. (그리고 선후 관계를 뜻하는 정수를 생산한다.)
Comparable은 언제나 소비자이므로, 일반적으로 Comparable<? super E>를 사용하는 편이 낫다.
Comparator 또한 언제나 소비자이므로 Comparator<? super E>라고 사용하는 것이 낫다.
수정된 max는 이 책에서 가장 복잡한 메서드 선언일 것이다.
그러나 이렇게까지 복잡하게 만들만한 가치가 충분히 있다.
다음 리스트는 오직 수정된 max로만 처리할 수 있다.
List<ScheduledFuture<?>> scheduledFutures = ...;
// 실제 선언 내용
public interface Comparable<E>;
public interface Delayed extends Comparable<Delayed>;
public interface ScheduledFuture<V> extends Delayed, Future<V>;
ScheduledFuture는 Comparable<ScheduledFuture>를 확장하지 않았기 때문에 처리할 수 없다.
하지만 ScheduledFuture의 상위 인터페이스인 Delayed는 Comparable<Delayed>를 확장하고 있다.
즉, 수정 전에는 ScheduledFuture가 Delayed 인스턴스와도 비교할 수 있기 때문에 max가 이 리스트를 거부한다.
더 일반화하자면, Comparable(혹은 Comparator)을 직접 구현하지 않고, 직접 구현한 다른 타입을 확장한 타입을 지원하기 위해서 와일드 카드가 필요한 것이다.
3. 타입 매개변수와 와일드 카드
타입 매개변수와 와일드 카드에는 공통되는 부분이 많다.
그래서 메서드를 정의할 때, 둘 중 어느 것을 사용해도 괜찮을 때가 많다.
📌 Ex. swap
public static <E> void swap(List<E> list, int i, int j);
public static void swap(List<?> list, int i, int j);
위 메서드 둘 다 큰 문제는 없지만, public API라면 두번째가 낫다.
어떤 리스트든 명시한 인덱스 원소들을 교환해 줄 것이고, 신경 써야 할 타입 매개변수도 없기 때문이다.
메서드 선언에 타입 매개변수가 한 번만 나오면 와일드 카드로 대체하라
비한정적 타입 매개변수라면 비한정적 와일드 카드로 바꾸고, 한정적 타입 매개변수라면 한정적 와일드 카드로 바꿔라.
하지만 두 번째 swap 선언에는 문제가 하나 있다.
public static void swap(List<?> list, int i, int j) {
list.set(i, list.set(j, list.get(i)));
}
위 코드는 방금 꺼낸 원소를 리스트에 넣을 수 없다는 컴파일 에러가 발생한다.
비한정적 와일드 카드 List<?> 컬렉션이 null만 넣을 수 있기 때문이다.
이를 해결하는 가장 좋은 방법은 실제 타입을 알려주는 private 도우미 메서드를 따로 작성하는 것이다.
public static void swap(List<?> list, int i, int j) {
helper(list, i, j);
}
private static <E> void helper(List<E> list, int i, int j) {
list.set(i, list.set(j, list.get(i)));
}
helper 메서드는 리스트가 List<E>이을 알기 때문에 리스트에서 꺼낸 값의 타입은 항상 E이고, E 타입 값이라면 리스트에 넣어도 type-safe 함을 알고 있다.
도우미 메서드의 시그니처는 앞에서 "public API로 쓰기에는 너무 복잡하다"는 이유로 버렸던 첫 번째 swap 메서드의 시그니처와 완전히 동일하다.