불공변 방식은 유연하지 못하다.
불공변은 쉽게 말하면, 하위 타입이 상위 타입으로 대체될 수 없는 것을 말한다. 즉, 제네릭 타입 매개변수가 서로 다른 타입일 경우, 컴파일 에러가 발생한다. 따라서 타입 안정성을 보장하지만, API 유연성이 제한된다.
예시 코드
public class Stack<E> {
public Stack() { ... }
public void push(E e) { ... }
public void pop() { ... }
public boolean isEmpty() { ... }
public void pushAll(Iterable<E> src) { // 유연성 제한
for (E e : src)
push(e);
}
public void popAll(Collection<E> dst) { // 유연성 제한
while(!isEmpty()) {
dst.add(pop());
}
}
}
public class Main {
public static void main(String[] args) {
Stack<Number> numberStack = new Stack<>();
Interable<Integer> integers = null;
numberStack.pushAll(integers);
Collection<Object> numberCollection = new ArrayList<>();
numberStack.popAll(numberCollection);
}
}
pushAll()과 popAll() 메소드는 Iterable src의 원소 타입과 Stack의 원소 타입이 동일할 경우에만 정상적으로 동작한다. 해당 main 메소드처럼 Number의 하위 타입인 Integer를 삽입하려고 할 경우, 컴파일 오류가 난다. 유연성 따위 없어져버린 ..
한정적 와일드 카드을 사용해 유연성을 늘리자.
원소의 생성자나 소비자용 입력 매개변수에 와일드카드 타입을 사용하면, API 유연성을 늘릴 수 있다.
| 표기 | 영문 용어 | 설명 |
| <?> | Unbounded Wildcards | 제한 없음 (모든 타입이 가능) |
| <? extends E> | Upper Bounded Wildcards | 상한 경계 와일드 카드 |
| <? super E> | Lower Bounded Wildcards | 하한 경계 와일드 카드 |
예시 코드
public class Stack<E> {
public Stack() { ... }
public void push(E e) { ... }
public void pop() { ... }
public boolean isEmpty() { ... }
public void pushAll(Iterable<? extends E> src) { // 한정적 와일드 카드
for (E e : src)
push(e);
}
public void popAll(Collection<E> dst) { // 한정적 와일드 카드
while(!isEmpty()) {
dst.add(pop());
}
}
}
public class Main {
public static void main(String[] args) {
Stack<Number> numberStack = new Stack<>();
Interable<Integer> integers = null;
numberStack.pushAll(integers);
Collection<Object> numberCollection = new ArrayList<>();
numberStack.popAll(numberCollection);
}
}
이제 위 코드는 정상적으로 동작하게 된다.
PECS(Producer-Extends, Consumer-Super) 공식
: 와일드 카드 타입의 객체를 생성할 때는 extends
와일드 카드 타입의 개체를 사용 또는 소비할 때는 super
즉, 매개변수화 타입 T가 생산자라면 <? extends T>를 사용하고, 소비자라면 <? super T>를 사용하라는 뜻..
주의 사항
public static <E> Set<E> union(Set<? extends E> s1, Set<? extends E> s2) {
Set<E> result = new HashSet<>(s1);
result.addAll(s2);
return result;
}
당연한 말이지만, 반환 타입에는 한정적 와일드 카드 타입을 사용하면 안 된다. 그렇게 되면 클라이언트 코드에서도 와일드 카드 타입을 써야하기 때문이다.
Comparator과 Comparable은 소비자다.
예시 코드
public static <E extends Comparable<E>> E max(List<E> list)
// 와일드 카드 타입으로 수정
public static <E extends Comparable<? super E>> E max(List<? extends E> List)
입력 매개변수에서는 E 인스턴스를 생산하므로 List<E>를 List<? extends E>로 수정하였다.
Comparable<E>는 E 인스턴스를 소비하므로 Comprable<E>를 Comparable<? super E>로 수정한다.
Comparator<E>보다는 Comparator<? super E>를 사용하는 것이 좋다.
Comparable(혹은 Comparator)을 직접 구현하지 않고, 직접 구현한 다른 타입을 확장한 타입을 지원하기 위해서 !
Comparator<? super E>를 사용하면 보다 유연한 정렬 기준을 만들 수 있다.
아래 구조로 생각해보면 쉽게 이해될 것이다.
public interface Comparable<E>
public interface Delayed extends Comparable<Delayed>
public interface ScheduledFuture<V> extends Delayed, Future<V>
ScheduledFuture<V>의 객체들을 정렬하려고 한다고 가정하자.
Comparator<ScheduledFuture<V>>를 사용할 경우, ScheduledFuture<V> 객체끼리의 정렬은 가능하지만, Delayed나 Future<V> 객체와의 비교는 허용되지 않는다.
Comparator<? super ScheduledFuture<V>>를 사용하면, Delayed나 Future 객체와의 비교가 가능하다. 즉, ScheduledFuture<V>의 상위 타입을 비교할 수 있다.
타입 매개변수 vs 와일드카드
public static <E> void swap(List<E> list, int i, int j);
public static void swap(List<?> list, int i, int j);
메서드를 정의할 때, 타입 매개변수와 와일드카드 중 어떤 것을 사용해도 괜찮을 때가 많다.
메서드 선언에 타입 매개변수가 한 번만 나오는 경우, 와일드카드를 사용하는 것이 좋다.
그러나 와일드카드가 사용된 리스트에는 null 외에 다른 값을 추가할 수 없다. 이를 해결하기 위해 도우미 메소드를 사용할 수 있다. 이 도우미 멕소드는 실제 타입을 처리하기 위해 제네릭이어야 한다.
아래 예시를 보자.
예시 코드
public static void swap(List<?> list, int i, int j) { // List<?>에는 null 외에는 어떤 값도 넣을 수 없다.
list.set(i, list.set(j, list.get(i)));
}
위 코드는 List<?>의 특성 때문에 컴파일 오류가 난다. 이는 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)));
}
위 내용을 정리하면,
타입 매개변수를 사용해야 하는 경우는 "메소드가 반환 값을 포함하여 일관된 타입 관계를 유지해야 할 경우" 혹은 "여러 매개변수들이 동일한 타입을 가져야 하는 경우"이다.
반대로 와일드카드를 사용해야 하는 경우는 "메서드 선언에 타입 매개변수가 한 번만 나오는 경우", "메소드가 입력 매개변수의 타입에 크게 의존하지 않을 경우"이다.
핵심 요약
불공변 방식은 타입 안정성을 보장하지만 API 유연성을 제한한다. 이를 해결하기 위해 한정적 와일드카드를 사용할 수 있으며, PECS(Producer-Extends, Consumer-Super) 공식을 기반으로 한다. 다만, 반환 타입에는 한정적 와일드카드를 사용하지 않아야 하고, Comparator과 Comparable은 소비자이다. 또한, 메소드 선언에 타입 매개변수가 한 번만 나오는 경우 와일드카드를 사용하는 것이 좋으며, 와일드카드가 사용된 리스트에 다른 값을 추가하려면 도우미 메소드를 사용해야 한다.
'Language > Java' 카테고리의 다른 글
| [effective java] 아이템 33. 타입 안전 이종 컨테이너를 고려하라. (0) | 2023.05.14 |
|---|---|
| [effective java] 아이템 32. 제네릭과 가변인수를 함께 쓸 때는 신중하라. (0) | 2023.05.14 |
| [effective java] 아이템 30. 이왕이면 제네릭 메서드로 만들라. (0) | 2023.05.09 |
| [effective java] 아이템 29. 이왕이면 제네릭 타입으로 만들라. (0) | 2023.05.09 |
| [effective java] 아이템 28. 배열보다는 리스트를 사용하라. (0) | 2023.05.09 |