매개변수화 타입은 불공변(invariant)이다. 즉 List<Type1>
은 List<Type2>
의 하위 타입도, 상위 타입도 아니다. 따져보면 말이 되는데 List<Object>
에는 어떤 객체든 넣을 수 있지만, List<String>
에는 문자열만 넣을 수 있다. List<String>
은 List<Object>
가 하는 일을 제대로 수행하지 못하니 하위 타입이 될 수 없다 (리스코프 치환 원칙 - 아이템 10)
하지만 이렇게 해서 불편한 점이 생기기도 한다.
public class Stack<E> {
public Stack();
public void push(E e);
public E pop();
public boolean isEmpty();
}
여기에 여러 원소를 스택에 넣는 메서드를 추가해보자.
와일드카드 타입을 사용하지 않은 pushAll 메서드 - 결함 존재
public void pusHAll(Iterable<E> src) {
for (E e : src)
push(e);
}
컴파일되지만 완벽하지 않다. Stack<Number>
로 선언 후 pushAll(intVal)
을 하는데 Integer
타입이다. Integer
는 Number
의 하위 타입이니 잘 될거라 생각한다. 하지만 불공변 때문에 오류가 뜬다.
이럴때 한정적 와일드카드 타입을 사용하면 된다.
E 생성자 매개변수에 와일드카드 타입 적용
public void pushAll(Iterable<? extends E> src) {
for (E e : src)
push(e);
}
E의 하위 타입의 Iterable
이라는 뜻이다.
popAll도 비슷하다.
와일드카드 타입을 사용하지 않은 popAll 메서드 - 결함 존재
public void popAll(Collection<E> dst) {
while (!isEmpty())
dst.add(pop());
컴파일은 문제 없어 보인다.
Stack<Number> numberStack = new Stack<>();
Collection<Object> objets = ... ;
numberStack.popAll(objects);
위 코드를 실행하면 "Collection<Object>는 Collection의 하위 타입이 아니다" 라는 오류가 발생한다.
이번에도 와일드카드로 해결이 가능한데, popAll의 입력 매개변수의 탕비이 "E의 Collection"이 아니라 "E의 상위 타입의 Collection"이어야 한다.
E consumer
public void popAll(Collection<? super E> dst) {
while (!isEmpty())
dst.add(pop());
}
유연성을 극대화하려면 원소의 생성자나 소비자용 입력 매개변수에 와일드카드 타입을 사용하라.
PECS: Producer-Extends, Consumer-Super
한편 입력 매개변수가 생성자와 소비자의 역할을 동시에 한다면 와일드카드 타입을 써도 좋을게 없다. 이때는 타입을 정확히 지정해야하므로 와일드카드가 적절하지 않다.
와일드카드가 잘 사용되면 받아들여야 할 매개변수를 받고 거절해야 할 매개변수는 거절하는 작업이 알아서 이뤄진다. 클래스 사용자가 와일드카드 타입을 신경 써야 한다면 그 API에 문제가 있을 가능성이 크다.
매개변수와 인수의 차이
매개변수(paramter)는 메서드 선언에 정의한 변수
인수는 메서드 호출 시 넘기는 '실젯값'
타입 매개변수와 와일드카드에는 공통되는 부분이 있어서, 메서드를 정의할 때 선택하기 어려울 때가 있다.
public static <E> void swap(List<E> list, int i, int j);
public static void swap(List<?> list, int i, int j);
어떤게 더 나을까? public API라면 간단한 두 번째가 낫다. 어떤 리스트든 이 메서드에 넘기면 명시한 인덱스의 원소들을 교환해준다.
메서드 선언에 타입 매개변수가 한 번만 나오면 와일드카드로 대체하라.
비한정적 타입 매개변수라면 비한정적 와일드카드로 바꾸고, 한정적 타입 매개변수라면 한정적 와일드카드로 바꾸면 된다.
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 static <E> void swapHelper(List<E> list, int i, int j) {
list.set(i, list.set(j, list.get(i)));
}
swapHelper 메서드는 리스트가 List<E>
임을 알고 있는 것이다.