Item31. 한정적 와일드카드를 사용해 API 유연성을 높이라
2022-01-03 20:32:58
Generic 타입 불공변 특성으로 인한 제약
public class Stack<E> {
private E[] elements;
private int size = 0;
private static final int DEFAULT_INITIAL_CAPACITY =16;
public Stack(){
elements = (E[]) new Object[DEFAULT_INITIAL_CAPACITY];
}
public void push(E e){
ensureCapacity();
elements[size++] =e;
}
public E pop(){
if(size ==0 ){
throw new EmptyStackException();
}
E result = elements[--size];
elements[size] =null;
return result;
}
public boolean isEmpty(){
return size == 0;
}
private void ensureCapacity(){
if(elements.length == size){
elements = Arrays.copyOf(elements,2*size+1);
}
}
}
Generic type Stack에서 pushAll method를 추가한다고 가정하자
public void pushAll(Iterable<E> src){
for (E e : src) {
push(e);
}
}
Integer는 Number의 하위 타입임으로 아래의 method도 논리적으로는 정상작동해야 올바르지만, Generic의 불변성때문에 허용되지않고 compile error 가 나온다.
public static void main(String[] args) {
Stack<Number> numberStack = new Stack<Number>();
Iterable<Integer> intList = List.of(10,11,12,13);
// numberStack.pushAll(intList);
// incompatible types: Iterable<Integer> cannot be converted to Iterable<Number>
}
bounded wildcard type을 통한 유연성 제공
// bounded wildcard type
List<? extends Number> // Number를 포함한 하위타입만 타입매개변수로 올수있다.
List<? super Number> // Number를 포함한 상위타입만 타입매개변수로 올수 있다.
한정적 와일드카드 타입 ( bounded wildcard type ) 을 통해 불공변 특성을 가진 Generic 의 유연성을 높일수 있다.
PECS - producer-extends , consumer-super 공식
- 와일드카드 타입을 사용하는 기본 원칙으로, 매개변수화 타입 T가 생성자라면 <? extends T> 를 사용하고 , 소비자라면 <? super T> 를 사용하라는 공식이다.
위 Generic stack의 예에 적용해보면 다음과 같다. pushAll의 매개변수인 Iterable은 값을 꺼내서(생성해주어서) stack에게 전달해주는 매개변수이다. 따라서 생성자라고 볼 수 있다. --> <? extends T> 를 사용한다.
// PECS 공식 - 생성자
public void pushAll(Iterable< ? extends E> src){
for (E e : src) {
push(e);
}
}
또 다른 stack의 method로 stack의 모든 원소를 차례로 꺼내서 매개변수에게 전달해주는 popAll() method를 예시로 들었다.
public void popAll(Collection<E> dst){
while (!isEmpty()){
dst.add(pop());
}
}
마찬가지로 generic은 불공변임으로 모든 객체의 부모class인 Object class임에도 불구하고 in-compatible type compile error 가 뜬다.
Stack<Number> numberStack = new Stack<Number>();
Collection<Object> objList = new ArrayList<>();
// numberStack.popAll(objList);
PECS 공식을 적용해서 Collection<E> dst 매개변수를 보면 stack의 원소들을 전달받는(stack의 원소를 소비해서) 매개변수이다.
따라서 다음과 같이 수정해야한다.
public void popAll(Collection<? super E> dst){
while (!isEmpty()){
dst.add(pop());
}
}
또 다른 예제로 collection에서 max값을 찾아주는 max method에 한정적 와일드카드 타입을 적용하면 다음과 같다.
public static <E extends Comparable<? super E>> E max(Collection<? extends E> c){
if(c.isEmpty()){
throw new IllegalArgumentException("empty collection");
}
E result = null;
for (E e : c) {
if(result == null || e.compareTo(result) > 0){
result = Objects.requireNonNull(e);
}
}
return result;
}
parameter는 collection에서 값을 생성해줌으로 extends , 반환타입인 comparable은 값을 소비함으로 super 를 적용하였다
- Comparable은 언제나 소비자임으로, 일반적으로 Comparable<E>보다는 Comparable<? super E> 를 사용하는 편이 낫다.
꼭 PECS 공식을 적용해서 코드를 복잡하게 만들어야 하는 이유가 있을까?

구체적인 예를 보면 아래의 list는 PECS 규칙을 적용한 max method에만 적용된다.
List<ScheduledFuture<?>> list;
public interface Comparable<E>
public interface Delayed extends Comparable<E>
public interface ScheduledFuture<V> extends Delayed,Future<V>
ScheduledFuture는 Comparable을 상속받은 Delayed Interface를 상속받았다. max method의 반환타입인 E extends Comparable<E> 를 보면 Comparable<ScheduledFuture> type은 존재할 수 없기 때문이다.
유의점
-
반환타입에서는 한정적 와일드카드타입을 사용하면 client에서도 한정적 와일드카드 타입을 써야함으로 반환타입에는 사용하면 안된다.
-
compiler 가 올바른 타입을 추론하지 못할떄는 명시적 타입 인수를 사용해서 타입을 알려주면 된다. 이는 JDK 8부터는 Target typing을 지원하지만 JDK 7까지는 발생할 수 있는 문제이다.
Target typing (Type Inference , Generalized Target-Type Inference ) : 타입 추론으로 말 그대로 compiler 가 타입을 추론해서 불필요한 boilerplate 코드를 줄여준다. (https://docs.oracle.com/javase/specs/jls/se8/html/jls-18.html)
// type inference 도입 전
Map<String, Map<String, String>> mapOfMaps = new HashMap<String, Map<String, String>>();
List<String> strList = Collections.<String>emptyList();
List<Integer> intList = Collections.<Integer>emptyList();
//후
Map<String, Map<String, String>> mapOfMaps = new HashMap<>();
List<String> strList = Collections.emptyList();
List<Integer> intList = Collections.emptyList();
타입 매개변수와 wildcard간 선택
public static <E> void swap(List<E> list,int i ,int j) // 타입매개변수 사용
public static void swap(List<?> list,int i ,int j) // 와일드카드 사용
generic을 활용해 method 선언시 wildcard와 타입매개변수중 어떤것을 사용해야 할까?
- method 선언에 타입 매개변수가 한번만 나오면 wild card로 대체하는 것을 권고한다.
위 예제는 하나의 매개변수만 나옴으로, wildcard를 선택하였으나, wildcard collection은 null이외의 값을 넣을 수 없다.
public static void swap(List<?> list,int i ,int j){
list.set(i,list.set(j,list.get(i)));
}
책에서는 해결방안으로 wildcard 의 실제 타입을 알려주는 private 도우미 method를 활용하라고 제시하고 있다.
public static void swap(List<?> list,int i ,int j){
swapHelper(list,i,j);
}
private static <E> void swapHelper(List<E> list,int i , int j){
list.set(i,list.set(j,list.get(i)));
}
정리
generic type에 wildcard type을 PECS 규칙을 사용해 적용하면 generic의 장점과 generic의 불공변 제약으로 부터 유연함을 동시에 취할 수 있다.
댓글
이 게시글에 대한 의견을 공유해주세요!
