유연성을 극대화하려면 원소의 생산자나 소비자용 입력 매개변수에 와일드카드 타입을 사용하라.
[Why]
매개변수화 타입은 불공변(invariant)이다. (아이템28)
- 서로 다른 타입 Type1과 Type2가 있을 때 List<Type1>은 List<Type2>의 하위 타입도 상위 타입도 아니다.
- List<String>은 List<Object>의 하위 타입이 아니라는 뜻인데, 곰곰이 따져보면 사실 이쪽이 말이 된다.
- List<String>은 List<Object>가 하는 일을 제대로 수행하지 못하니 하위 타입이 될 수 없다.
(리스코프 치환 원칙에 어긋난다. 아이템10)
하지만 때론 불공변 방식보다 유연한 무언가가 필요하다.
public void pushAll(Iterable<E> src) {
for (E e : src) {
push(e);
}
}
- 일련의 원소를 스택에 넣는 메서드이다.
- 이 메서드는 깨끗이 컴파일되지만 완벽하지 않다.
- Iterable src의 원소 타입이 스택의 원소 타입과 일치하면 잘 작동한다.
- 하지만 Stack<Number>로 선언한 후 pushAll(intVal)을 호출하면 어떻게 될까?
- Integer는 Number의 하위 타입이니 잘 동작해야 할 것 같지만, 오류 메시지가 뜬다. incompatible types
- 매개변수화 타입이 불공변이기 때문이다.
[How]
자바는 이런 상황에 대처할 수 있는 한정적 와일드카드 타입이라는 특별한 매개변수화 타입을 지원한다.
public void pushAll(Iterable<? extends E> src) {
for (E e : src) {
push(e);
}
}
- pushAll의 입력 매개변수 타입은 'E의 Iterable'이 아니라 'E의 하위 타입의 Iterable'이어야 하며,
와일드 카드 타입 Iterable<? extends E>가 정확히 이런 뜻이다.
(사실 extends라는 키워드는 이 상황에 딱 어울리지는 않는다. 하위 타입이란 자기 자신도 포함하지만 그렇다고 자신을 확장한 것은 아니기 때문이다. 아이템29)
public void popAll(Collection<? super E> dst) {
while (!isEmpty()) {
dst.add(pop());
}
}
- pushAll과 짝을 이루는 popAll 메서드다.
입력 매개변수가 생산자와 소비자 역할을 동시에 한다면 와일드카드 타입을 써도 좋을 게 없다.
- 타입을 정확히 지정해야 하는 상황으로, 이때는 와일드카드 타입을 쓰지 말아야 한다.
- 다음 공식을 외워두면 어떤 와일드카드 타입을 써야 하는지 기억하는 데 도움이 될 것이다.
펙스(PECS) : producer-extends, consumer-super - PECS 공식은 와일드카드 타입을 사용하는 기본 원칙이다. (겟풋 원칙, Get and Put Principle)
반환 타입에는 한정적 와일드카드 타입을 사용하면 안 된다.
- 유연성을 높여주기는커녕 클라이언트 코드에서도 와일드카드 타입을 써야 하기 때문이다.
예시) 아이템30의 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) // 수정된 버전
- 입력 매개변수 : E 인스턴스를 생산하므로 원래의 List<E>를 List<? extends E>로 수정했다.
- 타입 매개변수 : 원래 선언에서는 E가 Comparable<E>를 확장한다고 정의했는데, 이때 Comparable<E>는 E 인스턴스를 소비한다. 그래서 매개변수화 타입 Comparable<E>를 한정적 와일드카드 타입인 Comparable<? super E>로 대체했다.
- Comparable은 언제나 소비자이므로, 일반적으로 Comparable<E>보다는 Comparable<? super E>를 사용하는 편이 낫다.
- Comparator도 마찬가지로 Comparator<E>보다는 Comparator<? super E>를 사용하는 편이 낫다.
- 수정된 버전의 max는 이 책에서 가장 복잡한 메서드 선언일 것이다. 이렇게까지 복잡하게 만들만한 가치가 있을까?
- 답은 '그렇다'이다.
- 다음 리스트는 오직 수정된 max로만 처리할 수 있다. List<ScheduledFuture<?>> scheduledFutures = ...;
- 수정 전 max가 이 리스트를 처리할 수 없는 이유는 java.util.concurrent 패키지의 ScheduledFuture가 Comparable<ScheduledFuture>를 구현하지 않았기 때문이다.
- ScheduledFuture는 Delayed의 하위 인터페이스이고, Delayed는 Comparable<Delayed>를 확장했다.
- 다시 말해, ScheduledFuture의 인스턴스는 다른 ScheduledFuture 인스턴스 뿐 아니라 Delayed 인스턴스와도 비교할 수 있어서 수정 전 max가 이 리스트를 거부하는 것이다.
- 즉, Comparable(혹은 Comparator)을 직접 구현하지 않고, 직접 구현한 다른 타입을 확장한 타입을 지원하기 위해 와일드카드가 필요하다.
타입 매개변수와 와일드카드에는 공통되는 부분이 있어서, 메서드를 정의할 때 둘 중 어느 것을 사용해도 괜찮을 때가 많다.
public static <E> void swap(List<E> list, int i, int j);
public static void swap(List<?> list, int i, int j);
- 주어진 리스트에서 명시한 두 인덱스의 아이템들을 교환(swap)하는 정적 메서드이다.
- 첫번째는 비한정적 타입 매개변수(아이템30)를 사용했고, 두번째는 비한정적 와일드카드를 사용했다.
- public api라면 간단한 두번째가 낫다.
- 기본 원칙은 다음과 같다. 메서드 선언에 타입 매개변수가 한 번만 나오면 와일드카드로 대체하라.
public static void swap(List<?> list, int i, int j) {
list.set(i, list.set(j, list.get(i)));
}
// 에러 메시지
// list.set(i, list.set(j, list.get(i)));
// ^
- 하지만 두 번째 swap 선언에는 문제가 하나 있는데, 위와 같이 아주 직관적으로 구현한 코드가 컴파일되지 않는다는 것이다.
- 방금 꺼낸 원소를 리스트에 다시 넣을 수 없다니, 이게 대체 무슨 일인가?
- 원인은 리스트의 타입이 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>임을 알고 있다.
결론
swap 메서드 내부에서는 더 복잡한 제네릭 메서드를 이용했지만, 덕분에 외부에서는 와일드카드 기반의 멋진 선언을 유지할 수 있었다.
※ 매개변수(parameter)는 메서드 선언에 정의한 변수이고, 인수(argument)는 메서드 호출 시 넘기는 '실젯값'이다.
class Set<T> { ... }
Set<Integer> = ...;
여기서 T는 타입 매개변수가 되고, Integer는 타입 인수가 된다.
'독서찰기(讀書札記) > 이펙티브 자바' 카테고리의 다른 글
[아이템 33] 타입 안전 이종 컨테이너를 고려하라 (0) | 2022.02.16 |
---|---|
[아이템 32] 제네릭과 가변인수를 함께 쓸 때는 신중하라 (0) | 2022.02.15 |
[아이템 30] 이왕이면 제네릭 메서드로 만들라 (0) | 2022.02.12 |
[아이템 29] 이왕이면 제네릭 타입으로 만들라 (0) | 2022.02.09 |
[아이템 28] 배열보다는 리스트를 사용하라 (0) | 2022.02.07 |