[Effective Java] 아이템31. 한정적 와일드카드를 사용해 API 유연성을 높이라
1. What: Bounded Wildcards(한정적 와일드 카드)
와일드 카드
- 제네릭 코드에서 물음표(?)로 표기된 것을 가리키고 아직 알려지지 않은 타입을 나타낸다.
Unbounded Wildcards(비한정적 와일드 카드)
- 와일드 카드 문자인 물음표(?)만 사용할 때를 의미한다.
- 알려지지 않은 타입의 리스트라고 부른다.
Bounded Wildcards(한정적 와일드 카드)
- 와일드 카드(물음표, ?)에 범위를 제한하는 것을 한정적 와일드 카드라고 한다.
- extends를 활용한 한정적 와일드 카드와 super를 활용한 한정적 와일드 카드가 있다. 예시는 아래에서 설명하겠다!
2. Why: 매개변수화 타입은 기존 타입들의 상,하위 관계를 따르지 않기 때문에.
불공변 방식의 문제점
아래 Stack 클래스의 public API를 보자.
public class Stack<E> {
public Stack();
public void push(E e);
public E pop();
public boolean isEmpty();
}
여기에 일련의 원소를 스택에 넣는 메서드를 추가한다면 어떻게 될까?
public void pushAll(Iterable<E> src) {
for (E e : src)
push(e)
}
이 메서드는 컴파일은 되지만 문제점이 있다.
-> src의 원소타입이 Stack의 타입과 일치할 때는 정상적으로 작동한다.
-> 하지만 Stack의 하위타입을 넣고자 할 때는 오류가 발생한다.
예시) Stack<Number> 스택을 하나 선언하고, Number의 하위 타입인 Integer 값을 넣어보자
Stack<Number> numberStack = new Stack<>();
Iterable<Integer> integers = ...;
numberStack.pushAll(integers);
위와 같이 코드를 작성하면 호환되지 않는 타입(incompatible types) 이라고 컴파일 오류가 나온다.
매개변수화 타입이 불공변이기 때문이다!! ( = String이 Object의 하위타입이라고 List<String>이 List<Object>의 하위타입인 것은 아니다.)
참고)
1. 공변: 자기 자신과 자식 객체로의 타입 변환을 허용
Object[] before = new Long[1];
2. 불공변: List<String>과 List<Object>가 있을 때 두 개의 타입은 전혀 관련이 없다는 뜻이다.
출처) https://devlog-wjdrbs96.tistory.com/263
3. How: Get and Put Principle
한정적 와일드 타입을 사용해 위 불공변 문제를 해결해보자 (extends 사용)
자바는 이런 상황에 대처할 수 있는 한정적 와일드카드 타입이라는 특별한 매개변수화 타입을 지원한다.
pushAll의 입력 매개변수 타입은 'E의 Iterable'이 아니라 'E의 하위 타입의 Iterable'이어야 한다.
아래처럼 와일드카드 타입 Iterable<? extends E>로 사용하면 된다.
public void pushAll(Iterable<? extends E> src) {
for (E e : src)
push(e);
}
상위 타입을 허용해야 하는 케이스(super 사용)
아래와 같은 메서드가 추가되었다고 하자.
public void popAll(Collection<E> dst) {
when (!isEmpty())
dst.add(pop())
}
Strack<Number> numberStack = new Stack<>();
Iterable<Object> objects = ...;
numberStack.pushAll(objects);
컴파일하면 "Collection는 Collection의 하위 타입이 아니다"라는 비슷한 오류가 발생한다.
public void popAll(Collection<? super E> dst) {
when (!isEmpty())
dst.add(pop())
}
-> dst 컬렉션이 'E의 상위 타입의 Collection'이어야 하므로 super를 사용한 한정적 와일드 타입을 적용하면 해결할 수 있다.
*PECS: producer-extends, consumer-super
위 공식을 외워두면 어떤 와일드카드 타입을 써야 하는지 기억할 수 있다..!
즉, 매개변수화 타입 T가 생산자라면 <? extends T>를 사용하고, 소비자라면 <? super T>를 사용하라.
생산자, 소비자라는 말이 애매하게 느껴질 수 있다.
메서드에 전달된 매개변수를 사용해 새로운 결과를 낸다면 생산자,
메서드에 전달된 매개변수에 어떠한 결과를 전달하면 소비자라고 생각하면 된다.
예시) 아래처럼 Union을 구하는 메서드는 어떻게 수정해야할까?
public static <E> Set<E> union(Set<E> s1, Set<E> s2)
s1과 s2를 사용해 새로운 set을 만드니(= Set<E>의 생산자) 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);
참고) 이 코드가 자바 7이하에서는 통과되지 않는다. (자바 7이전에는 목표 타이핑을 지원하지 않았기 때문.)
컴파일러가 타입을 알지 못해 생기는 에러이기 때문에, 아래와 같이 명시적으로 타입 인수를 지정해주면 해결할 수 있다.
Set<Number> numbers = Union.<Number>union(integers, doubles);