서로 다른 Type1
과 Type2
가 있을 때 List<Type1>
과 List<Type2>
는 하위 타입도 상위 타입의 관계도 아니다. 예컨데, List<String>
은 List<Object>
의 하위 타입이 아니라는 의미이다. List<String>
에는 문자열만 넣을 수 있지만 List<Object>
는 어떤 객체든 넣을 수 있다.
List<String>
은 List<Object>
가 하는 일을 제대로 수행하지 못하니 (리스코프 치환 원칙을 위반) 하위 타입이 될 수 없다.
하지만 때론 불공변 방식보다 유연한 방식이 필요할 때가 있다. 이럴 때 한정적 와일드카드 타입이라는 특별한 매개변수화 타입이 유용하게 사용될 수 있다.
[와일드카드를 사용하지 않은 pushAll 메서드]
...
public void pushAll(Iterable<E> src) {
for (E e : src) {
push(E);
}
}
...
public static void main(String[] args) {
Stack<Number> numberStack = new Stack<>();
Iterable<Integer> iterable = ...;
numberStack.pushAll(iterable);
}
...
src
매개변수는 Stack
이 사용할 E인스턴스를 생산하므로 생산자라고 볼 수 있다.
Integer
는 Number
의 하위 타입이므로 논리적으로 잘 동작해야 할 것 같지만 실제로는 타입 변경할 수 없다는 에러가 발생한다.
이런 상황에 한정적 와일드카드 타입이 유용하게 사용될 수 있다.
[E 생산자(producer) 매개변수에 와일드카드 타입 적용]
...
public void pushAll(Iterable<? extends E> src) {
for (E e : src) {
push(E);
}
}
...
Iterable <? extends E>
는 E
의 Iterable
이 아니라 E
의 하위 타입의 Iterable
이어야 한다는 의미를 갖는다.
이렇게 한정적 와일드카드를 사용하면 타입이 안전해지므로 클라이언트 코드가 정상적으로 컴파일된다.
[와일드카드를 사용하지 않은 popAll 메서드]
...
public void popAll(Collection<E> dst) {
while (!isEmpty()) {
dst.add(pop());
}
}
...
public static void main(String[] args) {
Stack<Number> numberStack = new Stack<>();
Collection<Object> objects = ...;
numberStack.popAll(objects);
}
...
Collection<Object>
는 Collections<Number>
의 하위 타입이 아니기 때문에 오류가 발생한다. 이번 상황역시 와일드카드 타입으로 해결이 가능하다.[E 소비자 매개변수에 와일드카드 타입 적용]
...
public void popAll(Collection<? super E> dst) {
while (!isEmpty()) {
dst.add(pop());
}
}
...
dst
매개변수는 Stack
으로부터 E
인스턴스를 소비하므로 소비자라고 볼 수 있다.producer - extends
consumer - super
T
가 생산자(producer)라면 <? extends T>
를 사용한자.T
가 소비자(consumer)라면 <? super T>
를 사용하자.[PECS 공식 사용 전]
public static <E> Set<E> union(Set<E> s1, Set<E> s2)
s1
과 s2
는 생산자이니 PECS 공식에 따라 생산자 와일드카드 방식에 따라 사용해야 한다.[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);
와일드카드가 제대로 사용된다면 사용자는 와일드 카드가 사용된지도 모른다. 만약 사용자가 와일드카드 타입을 신경써야 한다면 그 API는 문제가 있을 수 있다.
참고로, 만약 자바 버전이 7버전이라면 위 클라이언트 코드에서 명시적 타입 인수를 사용해주어야 한다. 자바 8이전까지는 타입 추론 능력이 충분하지 못해 반환타입을 명시해야 했다.
[자바 7까지는 명시적 타입 인수를 사용해야 했다.]
Set<Number> numbers = Union.<Number>union(integers,doubles);
매개변수는 메서드 선언에 정의한 변수이고, 인수는 메서드 호출 시 넘기는 실젯값이다.
[매개변수와 인수 예시]
void add(int value) {...}
add(10);
위 코드에서 value는 매개변수이고 10은 인수다.
[제네릭 매개변수와 인수 예시]
class Set<T> {...}
Set<Integer> = {...}
여기서 T는 타입 매개변수가 되고, Integer는 타입 인수가 된다.
기본 규칙: 메서드 선언에 타입 매개변수가 한번만 나오면 와일드 카드로 대체하라
[swap 메서드의 두 가지 선언 - 비한정적 타입 매개변수, 비한정적 와일드 카드]
public static <E> void swap(<List<E> list, int i, int j);
public static void swap(List<?> list, int i, int j);
이때, 한정적 타입 매개변수라면 한정적 와일드카드로 비한정적 타입 매개변수라면 비한정적 와일드카드로 변경하면 된다.
[직관적으로 구현한 코드 - 문제발생]
public static void swap(List<?> list, int i, int j){
list.set(i, list.set(j, list.get(i));
}
이 코드는 list.get(i)
로 꺼낸 코드를 다시 리스트에 넣을 수 없다는 오류를 발생시킨다. 이 오류는 제네릭 메서드인 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
타입의 값은 해당 List
에 다시 넣어도 안전함을 알고 있다.
유연성을 극대화하려면 원소의 생산자나 소비자용 입력 매개변수에 와일드카드 타입을 사용하자. 생산자(producer)는 extends
를 소비자(consumer)는 super
를 사용한다.
Comparable
과 Comparator
는 소비자라는 사실도 기억하자.