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

2023. 12. 7. 23:22Book/이펙티브 자바

 

 

아이템 28에서 이야기했듯 매개변수화 타입은 불공변이다.

 => List<type1> != List<type2>

 

리스코프 치환원칙을 생각해보면 불공변인게 정상이다.

 => List<String>은 List<Object>가 하는 일을 제대로 수행을 하지 못하니 하위 타입이 될 수 없다.

 

하지만 불공변 방식은 유연하지 못하다. 다음 Stack 예제를 보자

public class Stack<E> {
	pubilc 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의 원소 타입이 Number이고 src의 원소 타입이 Integer라면 타입이 불일치해 컴파일을 하지 못한다.

 

Number는 Integer의 상위 타입이니 들어와도 전혀 문제가 없고 더 유연하게 사용할 수 있다. 이렇게 쓰려면 한정적 와일드카드 타입이라는 특별한 매개변수를 사용해야 한다.

 

public void pushAll(Iterable<? extends E> src) {
    for (E e : src)
        push(e);
}

 

이렇게 메서드의 매개변수를 수정하면 다음과 같은 동작이 가능해진다.

public static void main(String[] args) {
    Stack<Number> numberStack = new Stack<>();
    Iterable<Integer> integers = Arrays.asList(3, 1, 4, 1, 5, 9);
    numberStack.pushAll(integers);
}

 

popAll 메서드도 보자. 이 메서드는 매개변수에 모든 원소를 담아 리턴하는 메서드이다.

 

public void popAll(Collection<E> dst) {
    while (!isEmpty())
        dst.add(pop());
}

 

이번에도 주어진 컬렉션의 원소 타입이 스택의 원소 타입과 일치한다면 문제없이 동작한다.

하지만 이번에도 유연하지 못한 부분이 있다.

Stack<Number>의 원소를 Collection<Object>로 담지 못한다.

Object는 Number의 상위 타입이니 Number의 원소가 Object 컬렉션에 들어가도 전혀 문제가 없다.

이번에도 와일드 카드가 문제를 해결해준다.

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

 

'E의 상위 타입의 Collection' 이라는 의미로 더욱 유연하게 API 를 사용할 수 있다.

 

다음 공식을 외워두면어떤 와일드카드 타입을 써야 하는지 도움이 된다.

 -PESC: Producer-Extends, Consuper-Super

 

Producer-Extends

 - Object의 컬렉션 Number나 Integer를 넣을 수 있다.

 - Number의 컬렉션에 Integer를 넣을 수 있다.

 - 입력 매개변수로부터 원소를 꺼내 컬렉션에서 이용

 

Consuper-Super

 - Integer의 컬렉션의 객체를 꺼내서 Number의 컬렉션에 담을 수 있다.

 - Number나 Integer의 컬렉션의 객체를 꺼내서 Object의 컬렉션에 담을 수 있다.

 - 컬렉션에서 원소를 꺼내 입력 매개변수에서 이용

 

 

책에서는 타입 매개변수와 와일드카드에는 공통되는 부분이 있어서, 메서드를 정의할 때 둘 중 어느 것을 사용해도 괜찮을 때가 많다고 한다.(개인적으로 와일드카드 혼자서만 쓰는건 구현부를 숨긴 것 말고는 큰 의미가 없어보인다...)

 

List에서 명시한 인덱스의 값을 바꿔주는 정적 메서드를 두 가지 방식으로 정의해보자

public static void swap(List<?> list, int i, int j);
public static <E> void swap(List<E> list, int i, int j);

 

둘 다 장단이 있다.

먼저 첫 번째는 메서드 시그니처가 간단해 보인다는 점이다. 하지만 문제는 구현부이다.

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

 

list의 get 메소드는 문제가 없는데 set 메소드는 요청 매개변수 타입을 알 수가 없어 컴파일 에러가난다. 그래서 헬퍼 클래스를 같이 사용해줘야 한다.

public static void swap(List<?> list, int i, int j) {
    swapHelper(list, i, j);
}

// 와일드카드 타입을 실제 타입으로 바꿔주는 private 도우미 메서드
private static <E> void swapHelper(List<E> list, int i, int j) {
    list.set(i, list.set(j, list.get(i)));
}

 

타입 매개변수 메서드 구현부와 비교해보자

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

 

내 생각엔 굳이 헬퍼 클래스까지 사용해가면서 써야하나? 하는 생각이 든다.