반응형
이 포스팅에서 작성하는 내용은 EFFECTIVE JAVA(이펙티브자바) 에서 발췌하였습니다.
아이템 31. 한정적 와일드카드를 사용해 API 유연성을 높이라
매개변수화 타입은 불공변이다. 즉, 서로 다른 타입 Type1과 Type2가 있을 때 List<Type1>은 List<Type2>의 하위 타입도 상위 타입도 아니다.
즉, List<String>은 List<Object>의 하위 타입이 아니라는 뜻
(List<Object>는 어떤 객체든 넣을 수 있지만, List<String>에는 문자열만 넣을 수 있기 때문)
불공변 방식보다 유연해지기
ex 1) Stack 클래스
public class Stack<E> {
public Stack();
public void push(E e);
public E pop();
public boolean isEmpty();
}
위의 코드에 일련의 원소를 스택에 넣는 메서드를 추가해보자.
ex 2) 위의 Stack 클래스에 추가할 pushAll 메서드
public void pushAll(Iterable<E> src) {
for (E e : src)
push(e);
}
}
- 위의 메서드는 컴파일되지만, 아래의 코드일 경우 오류가 발생한다.
// Integer는 Number의 하위 타입이기에 잘 될 것이라고 생각되지만, 오류가 발생
Strack<Number> numberStack = new Stack<>();
Iterable<Integer> intergers = ...;
numberStack.pushAll(integers);
- 이러한 경우에 대처할 수 있는 한정적 와일드카드 타입이라는 특별한 매개변수화 타입이 존재한다.
- pushAll의 입력 매개변수 타입은 ‘E의 Iterable’이 아니라 ‘E의 하위 타입의 Iterable’이어야 하며, 와일드카드 타입 Iterable<? extends E>가 정확히 이런 뜻이다.
ex 3) 와일드카드 타입을 적용한 psuhAll 메서드
public void pushAll(Iterable<? extends E> src) {
for (E e : src)
push(e);
}
위의 소스로 Stack은 물론 이를 사용하는 클라이언트 코드도 정상적으로 컴파일 된다.
그렇다면, pushAll과 반대로 Stack 안의 모든 원소를 주어진 컬렉션으로 옮겨 담는 popAll 메서드를 추가해보자.
ex4) 반대로 Stack 안의 모든 원소를 주어진 컬렉션으로 옮겨 담는 popAll 메서드
public void popAll(Collection<E> dst) {
when (!isEmpty())
dst.add(pop())
}
- 위의 경우에도, 주어진 컬렉션의 원소 타입이 스택의 원소 타입과 일치한다면 정상적으로 작동한다.
- 하지만, 아래의 경우에도 오류가 발생한다.
// Collection는 Collection의 하위 타입이 아니라는 오류 발생
Stack<Number> numberStack = new Stack<>();
Iterable<Object> objects = ...;
numberStack.pushAll(objects);
- 이 또한, 와일드카드 타입을 사용하면 해결할 수 있다.
- popAll의 입력 매개변수의 타입이 ‘E의 상위 타입의 Collection이어야 한다’의 의미를 가진 Collection<? super E>를 사용하면 된다.
ex 5) 와일드카드 타입을 적용한 popAll 메서드
public void popAll(Collection<? super E> dst) {
when (!isEmpty())
dst.add(pop())
}
- 이렇듯, 유연성을 극대화하려면 원소의 생산자나 소비자용 입력 매개변수에 와일드카드 타입을 사용하면 된다.
- 한편, 입력 매개변수가 생산자와 소비자 역할을 동시에 하는 경우엔 와일드카드 타입을 써도 좋은 건 없다.
PECS : Producer-Extends, Consumer-super
- 매개변수화 타입 T가 생산자(Producer)라면 <? extends T>를 사용하고, 소비자(Consumer)라면 <? super T>를 사용하라는 의미
- 생성자의 반환타입에는 한정적 와일드카드 타입을 사용하면 안된다. (클라이언트코드에서도 와일드카드 타입을 써야하기 때문) 클래스 사용자가 와일드카드 타입을 신경써야 한다면 그 API에는 무슨 문제가 있을 가능성이 크다.
자바 7 이전 사용방법
- 위의 코드들은 자바 8부터 정상적으로 컴파일되지만, 자바 7까지는 타입 추론 능력이 좋지 않아 문맥에 맞는 반환 타입(혹은 목표 타입)을 명시해야 한다.
- 컴파일러가 올바른 타입을 추론하지 못하는 경우 명시적 타입 인수를 사용해서 타입을 알려주면 된다.
ex 6) 명시적 타입 인수를 사용한 예
public static <E> Set<E> union(Set<? extends E> s1, Set<? extends E> s2)
// 자바 7까지의 명시적 타입 인수 사용
Set<Number> numbers = Union.<Number>union(integers, doubles);
타입 매개변수 vs 와일드카드
- 타입 매개변수와 와일드카드에는 공통되는 부분이 있어 메서드를 정의할 때 둘 중 어느 것을 사용해도 괜찮을 때가 많다.
- 예를 들어, 주어진 리스트에서 명시한 두 인덱스의 아이템들을 교환(Swap) 하는 정적 메서드를 두 방식 모두 정의해보자.
ex 7) 타입 매개변수와 와일드카드 방식을 이용한 swap 메서드
// 타입 매개변수 방식
public static <E> void swap(List<E> list, int i, int j);
// 와일드카드 방식
public static void swap(List<?> list, int i, int j);
- 메서드 선언에 타입 매개 변수가 한 번만 나오면 와일드카드로 대체하는 것이 좋다.
- 이 때 비한정적 타입 매개변수라면 비한정적 와일드 카드로 바꾸고, 한정적 타입 매개변수라면 한정적 와일드카드로 바꾸면 된다.
- 위 코드에서 와일드카드 방식으로 구현한 코드가 컴파일 되지 않는다.
- 원인 : 리스트의 타입이 List<?>인데 List<?>에는 null 외에 어떤 값도 넣을 수 없기 때문
- 해결 : ex 8의 코드 처럼 private 도우미 메서드를 따로 작성하여 활용
ex 8) private 도우미 메서드 예
public static void swap(List<?> list, int i, int j) {
swapHelper(i, list.set(j, list.get(i));
}
// 와일드카드 타입을 실제 타입으로 바꿔주는 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이고, E타입의 값이라면 이 리스트에 넣어도 안전한 것이 확실하다.
정리
- 복잡하지만, 와일드카드 타입을 적용하면 API가 훨씬 유연해질 수 있다.
- PECS 공식을 기억하자 (생산자 : extends, 소비자 : super)
- Comparable과 Comparator는 모두 소비자이다.
반응형
'BE > Java' 카테고리의 다른 글
[Effective Java] 33. 타입 안전 이종 컨테이너를 고려하라 (0) | 2022.10.19 |
---|---|
[Effective Java] 아이템 32. 제네릭과 가변인수를 함께 쓸 때는 신중하라 (1) | 2022.10.18 |
[Effective Java] 아이템 30. 이왕이면 제네릭 메서드로 만들라 (1) | 2022.10.13 |
[Effective Java] 아이템 29. 이왕이면 제네릭 타입으로 만들라 (0) | 2022.10.12 |
[Effective Java] 아이템 28. 배열보다는 리스트를 사용하라 (0) | 2022.10.11 |