Item31. 한정적 와일드카드를 사용해 API 유연성을 높이라

Generic 타입 불공변 특성으로 인한 제약

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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를 추가한다고 가정하자

1
2
3
4
5
public void pushAll(Iterable<E> src){
for (E e : src) {
push(e);
}
}

Integer는 Number의 하위 타입임으로 아래의 method도 논리적으로는 정상작동해야 올바르지만, Generic의 불변성때문에 허용되지않고 compile error 가 나온다.

1
2
3
4
5
6
7
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을 통한 유연성 제공

1
2
3
// 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> 를 사용한다.

1
2
3
4
5
6
// PECS 공식 - 생성자 
public void pushAll(Iterable< ? extends E> src){
for (E e : src) {
push(e);
}
}

또 다른 stack의 method로 stack의 모든 원소를 차례로 꺼내서 매개변수에게 전달해주는 popAll() method를 예시로 들었다.

1
2
3
4
5
public void popAll(Collection<E> dst){
while (!isEmpty()){
dst.add(pop());
}
}

마찬가지로 generic은 불공변임으로 모든 객체의 부모class인 Object class임에도 불구하고 in-compatible type compile error 가 뜬다.

1
2
3
Stack<Number> numberStack = new Stack<Number>();
Collection<Object> objList = new ArrayList<>();
// numberStack.popAll(objList);

PECS 공식을 적용해서 Collection<E> dst 매개변수를 보면 stack의 원소들을 전달받는(stack의 원소를 소비해서) 매개변수이다.

따라서 다음과 같이 수정해야한다.

1
2
3
4
5
public void popAll(Collection<? super E> dst){
while (!isEmpty()){
dst.add(pop());
}
}

또 다른 예제로 collection에서 max값을 찾아주는 max method에 한정적 와일드카드 타입을 적용하면 다음과 같다.

1
2
3
4
5
6
7
8
9
10
11
12
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에만 적용된다.

1
List<ScheduledFuture<?>> list;
1
2
3
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)

1
2
3
4
5
6
7
8
9
// 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간 선택

1
2
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이외의 값을 넣을 수 없다.

1
2
3
public static  void swap(List<?> list,int i ,int j){
list.set(i,list.set(j,list.get(i)));
}

책에서는 해결방안으로 wildcard 의 실제 타입을 알려주는 private 도우미 method를 활용하라고 제시하고 있다.

1
2
3
4
5
6
7
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의 불공변 제약으로 부터 유연함을 동시에 취할 수 있다.

Comments