이펙티브 자바 - item 31. 한정적 와일드카드를 사용해 API 유연성을 높이라
on JAVA
이펙티브 자바 - item 31. 한정적 와일드카드를 사용해 API 유연성을 높이라
로 타입으로 작성된 두 집합의 합집합을 반환하는 메서드
클래스와 마찬가지로 메서드도 제너릭으로 만들 수 있다.
와일드카드 타입을 사용하지 않는 스택
public void pushAll(Iterable<E> src) {
for (E e : src) {
push(e);
}
}
public void popAll(Collection<E> dst){
while (!isEmpty())
dst.add(pop());
}
public static void main(String[] args) {
//pushAll
StackV1<Number> numberStackV1 = new StackV1<>();
List<Number> numVal = List.of(1, 2, 3.4, 4L);
List<Integer> intVal = List.of(5, 6);
// 정상 동작
numberStackV1.pushAll(numVal);
// incompatible types: java.util.List<java.lang.Integer> cannot beconverted to java.lang.Iterable<java.lang.Number>
numberStackV1.pushAll(intVal);
//popAll
Collection<Object> objects = new ArrayList<>();
//java: incompatible types: java.util.Collection<java.lang.Object>cannot be converted to java.util.Collection<java.lang.Number>
numberStackV1.popAll(objects);
}
위와 같은 와일드카드 타입을 사용하지 않는 메서드를 실행시키려고 하면 위와 같은 에러가 발생한다.
이는 불공변인 제네릭을 매개변수 타입으로 사용했기 때문이다. 이럴때 한정적 와일드카드타입이라는 특별한 매개변수화 타입을 이용하면 이를 해결할 수 있다.
한정적 와일드카드 타입을 사용한 스택
public void pushAll(Iterable<? extends E> src) { // 한정적 와일드카드 적용
for (E e : src) {
push(e);
}
}
public void popAll(Collection<? super E> dst){ // 한정적 와일드카드 적용
while (!isEmpty())
dst.add(pop());
}
pushAll의 입력 매개변수 타입은 ‘E의 Iterable 이 아니라 ‘E의 하위 타입위 Iterable’이어야 하며, 와일드카드 타입 Iterable<? extends E>
가 정확히 이런 뜻이다.
popAll의 입력 매개변수 타입은 ‘E의 Collection’이 아니라 ‘E의 상위 타입의 Collection’이어야 한다. 이를 와일드카드 타입을 사용하면 Collection<? super E>
로 표현할 수 있다.
이와 같이 유연성을 극대화하려면 원소의 생산자나 소비자용 입력 매개변수에 와일드카드 타입을 사용할 수 있다. 하지만 입력 매개변수가 생산자와 소비자 역할을 동시에 한다면 와일드카드 타입을 사용하면 안된다.
어떤 와일드카드 타입을 사용해야하는가?
팩스(PECS) 공식에 따라 어떤 와일드카드 타입을 써야하는지 결정할 수 있다.
매개변수화 타입 T가 생산자라면
<? extends T>
를 사용한다.배개변수화 타입 T가 소비자라면
<? super T
를 사용한다.
PECS 공식에 따라 item-30의 union 함수를 수정하면 아래와 같다.
// 반환 타입은 여전히 Set<E>임에 주목
// 반환 타입에는 한정적 와일드카드 타입을 사용하면 안 된다.
// 유연성을 높여주기는 커녕 클라이언트 코드에서도 와일드카드 타입을 써야하기 때문
public static <E> Set<E> union(Set<? extends E> s1, Set<? extends E> s2) { //s1, s2 둘 다 생산자이므로 <? extends>를 사용
Set<E> result = new HashSet<>(s1);
result.addAll(s2);
return result;
}
public static void main(String[] args) {
Set<Integer> integers = Set.of(1, 3, 5);
Set<Double> doubles = Set.of(2.0, 4.0, 6.0);
Set<Number> union = union(integers, doubles);
}
받아들여야 할 매개변수를 받고 거절해야 할 매겨변수는 거절하는 작업이 알아서 이뤄진다. 클래스 사용자가 와일드카드 타입을 신경 써야 한다면 그 API에 무슨 문제가 있을 가능성이 크다.
한정적 와일드카드 타입의 복잡한 사용법
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
를 List<? extends E>로 수정한다. Comparale은 언제나 소비자이므로, 일반적으로
Comparable<? super E>
를 사용한다.마찬가지로 Comparator 또한 일반적으로
Comparator<? super E>
를 사용한다.
Comparable(혹은 Comparator)을 직접 구현하지 않고, 직접 구현한 다른 타입을 확장한 타입을 지원하기 위해서는 위와 같이 와일드카드가 필요하다.
수정전 max() 메서드는 List<ScheduledFuture<?» scheduledFutures = …;를 처리할 수 없다. ScheduledFuture는 Delayed의 하위 인터페이스이고, Delayed는 Comparable를 확장했다. 즉, ScheduledFuture의 인스턴스는 다른 ShceduledFuture 인스턴스뿐 아니라 Delayed 인스턴스와도 비교할 수 있기 때문에 수정 전 max가 List<ScheduledFuture<?»를 거부한다. 아래의 상속 관계와 함께 생각해보자.
타입 매개변수와 와일드카드
메서드 정의 시 비한정적 타입 매개변수와 비한정적 와일드 카드 중 어떤 방법이 좋을까?
class Example {
public static <E> void swap(List<E> list, int i, int j); // 비한정적 타입 매개변수 사용
public static void swap(List<?> list, int i, int j); // 비한정적 와일드 카드 사용
}
public API를 정의하는 경우 두 번째 방법을 사용하는 것이 좋다. 어떤 리스트든 이 메서드에 넘기면 명시한 인덱스의 원소들을 교환해 줌과 동시에 신경 써야 할 타입 매개변수도 없다.
- 기본 규칙
- 메서드 선언에 타입 매개변수가 한 번만 나오면 와일드 카드로 대체하라
- 이때 비한정적 타입 매개변수인 경우 비한정적 와일드카드로 바꾸고, 한정적 타입 매개변수라면 한정적 와일드카드로 바꾸면 된다.
두 번째 방법을 사용하는 경우 주의사항
List<?>에는 null 외에 어떤 값도 넣을 수 없어 방금 꺼낸 원소를 리스트에 다시 넣을 수 없는 오류를 발생시키면서 컴파일 되지 않는다.
public static void swap(List<?> list, int i, int j) {
list.set(i, list.set(j, list.get(i)));
}
위 문제를 형변환이나 리스트의 로 타입을 사용하지 않고 해결하기 위해서는 와일드 카드 타입의 실제 타입을 알려주는 메서드를 private 도우미 메서드로 따로 작성하여 사용해야한다.
실제 타입을 알아내려면 이 도우미 메서드는 제네릭 메서드여야 한다.
public static void swap(List<?> list, int i, int j) {
swapHelper(list, i, j);
}
// 와일드카드 타입을 실제 타입으로 바꿔주는 private 도우미 메서드
public static <E> void swapHelper(List<E> list, int i, int j) {
list.set(i, list.set(j, list.get(i)));
}
swapHelper 메서드는 리스트가 List임을 알고 있다.
즉, 이 리스트에서 꺼낸 값의 타입은 항상 E이고, E 타입의 값이라면 이 리스트에 넣어도 안전함을 알고있다.
swap 메서드를 호출하는 클라이언트는 복잡한 swapHelper의 존재를 모른 채 그 혜택을 누리는 것이다.
PECS 공식
PECS는 Java 제네릭스(Generics)에서 사용되는 약어로, Producer(생산자)와 Consumer(소비자)의 역할을 설명하는데 사용됩니다. 이는 Java의 제네릭 타입에서 타입 매개변수를 사용할 때 자주 등장하는 개념입니다.
PECS란?
P - Producer의 역할을 나타내는 타입 매개변수를 의미합니다. 이는 데이터를 > 생성하거나 반환하는 메서드에 사용되는 타입입니다.
E - Element(요소)의 약자로, 컬렉션의 요소 타입을 나타냅니다.
C - Consumer의 역할을 나타내는 타입 매개변수를 의미합니다. 이는 데이터를 > 소비하는 메서드에 사용되는 타입입니다.
S - Super의 역할을 나타내는 타입 매개변수를 의미합니다. 이는 데이터를 > 저장하거나 받아들이는 메서드에 사용되는 타입입니다.
PECS 원칙
- Producer extends Consumer super: 이 원칙은 “생산자는 확장(extends)되고, 소비자는 super로 제한”됩니다. 즉, 메서드가 데이터를 생성(생산)할 때는 Producer로 제한하고, 메서드가 데이터를 사용(소비)할 때는 Consumer로 제한합니다.
- Producer는 데이터를 생성하기 때문에 Producer가 소비할 수 있는 데이터 타입보다 더 구체적인 타입으로 제한됩니다.
- Consumer는 데이터를 사용하기 때문에 Consumer가 생산할 수 있는 데이터 타입보다 더 일반적인 타입으로 제한됩니다.
핵심 정리
- 조금 복잡하더라도 와일드 카드 타입을 적용하면 API가 훨씬 유연해진다.
- 널리 쓰일 라이브러리를 작성한다면 반드시 와일드카드 타입을 적절히 사용해야 한다.
- 생산자(producer)는 extends를 소비자(consumer)는 super를 사용한다.
- Comparable과 Comparator는 모두 소비자라는 사실을 잊지 않아야 한다.