이펙티브자바, 31. 한정적 와일드카드를 사용해 API 유연성을 높이라
와일드카드와 extends, super
- 제네릭
는 불공변이다. E의 매개변수가 Number이며 실제 삽입되는 인자가 그것의 하위 타입인 Interger라 하더라도, 형변환이 허용되지 않는다. - 와일드카드 <?>는 데이터에 대한 비교와 더불어 형변환을 위한 기능도 제공한다. 아래의 코드를 사용하여 하위타입에 대한 유연성을 높혀준다.
- <? extends E> 생산자
- <? super E> 소비자
- 전자는 매개변수가 생산자일 때 동작하며, 생산자란 데이터를 내보내는 경우를 의미한다.
- 후자는 매개변수가 소비자일 때 동작하며, 소비자란 데이터를 변경하는 경우를 의미한다. 기본 행태의 와일드 카드<?>로 받은 인자의 데이타를 변경할 수 없는 것과 대비하여, <? super E>는 그것의 인자의 데이터를 변경할 수 있다.
- 해당 코드는 아래와 같다. Stack을 활용한다.
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();
final 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);
}
public void pushAll(Iterable<E> src){
for (E e : src) {
push(e);
}
}
// 생산자, 데이터를 내보낸다.
public void pushAll2(Iterable<? extends E> src){
for (E e : src) {
push(e);
}
}
public void popAll(Collection<E>dst){
while(!isEmpty())
dst.add(pop());
}
// 그냥 와일드카드<?>와 달리 데이터를 넣어도 정상 동작한다.
// 소비자, 데이터를 받는다.
public void popAll2(Collection<? super E>dst){
while(!isEmpty())
dst.add(pop());
}
}
- 위에는 pushAll과 popAll이 있다.
- pushAll을 테스트하면 아래와 같다.
@Test
void test_pushAll(){
Stack<Number> numberStack = new Stack<>();
Iterable<Integer> integers = Arrays.asList(3, 1, 4, 1, 5, 9);
// 제네릭은 상위 하위 타입 간 형변환이 불가능하다. 그러므로 컴파일 에러가 발생한다.
// numberStack.pushAll(integers);
// 생산자의 하위 타입을 허용하는 <? extends E>를 통해 Number의 하위 타입인 Integer를 인자로 허용한다.
numberStack.pushAll2(integers);
}
@Test
void test_popAll(){
//given
Stack<Number> numberStack = new Stack<>();
Iterable<Integer> integers = Arrays.asList(3, 1, 4, 1, 5, 9);
numberStack.pushAll2(integers);
// when
Collection<Object> obj = new ArrayList<>();
// 위와 같은 이유로 컴파일 에러가 발생한다.
// numberStack.popAll(obj);
// 소비자의 하위 타입을 허용하는 <? super E>를 통해 하위타입의 인자를 허용한다.
numberStack.popAll2(obj);
// then
for (Object o : obj) {
System.out.println("o = " + o);
}
}
와일드카드와 제너릭의 조합
- 만약 super 혹은 extends 없이 모든 데이타타입에 대하여 허용하고자 하면 어떻게 해야하는가?
- 와일드카드는 앞서 말한 바와 같이 인자를 변경할 수 없다. 이로 인하여 컴파일 에러가 발생한다.
public class Swap {
public static void swapOld(List<?> list, int i, int j) {
list.set(i, list.set(j, list.get(i)));
}
@Test
void test(){
List<Integer> argList = Arrays.asList(3, 45, 6, 32, 234, 46);
// 컴파일 에러가 발생한다.
swapOld(argList, 0, argList.size() - 1);
}
}
- 방법은 매서드를 이중화 한다. 외부 API메서드는 와일드카드로 모든 인자를 받고, 내부 메서드는 이미 데이터 타입을 알기 때문에(
<E>
void) 제네릭으로 데이터를 변경할 수 있다.
public class Swap {
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)));
}
@Test
void test(){
List<Integer> argList = Arrays.asList(3, 45, 6, 32, 234, 46);
swap(argList, 0, argList.size() - 1);
System.out.println(argList);
}
}