[이펙티브 자바] 아이템 31. 한정적 와일드카드를 사용해 API 유연성을 높이라

June·2022년 3월 20일
0

[이펙티브자바]

목록 보기
29/72

불공변

매개변수화 타입은 불공변(invariant)이다. 즉 List<Type1>List<Type2>의 하위 타입도, 상위 타입도 아니다. 따져보면 말이 되는데 List<Object>에는 어떤 객체든 넣을 수 있지만, List<String>에는 문자열만 넣을 수 있다. List<String>List<Object>가 하는 일을 제대로 수행하지 못하니 하위 타입이 될 수 없다 (리스코프 치환 원칙 - 아이템 10)

하지만 이렇게 해서 불편한 점이 생기기도 한다.

public class Stack<E> {
    public Stack();
    public void push(E e);
    public E pop();
    public boolean isEmpty();
}

여기에 여러 원소를 스택에 넣는 메서드를 추가해보자.

와일드카드 타입을 사용하지 않은 pushAll 메서드 - 결함 존재

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

컴파일되지만 완벽하지 않다. Stack<Number>로 선언 후 pushAll(intVal)을 하는데 Integer 타입이다. IntegerNumber의 하위 타입이니 잘 될거라 생각한다. 하지만 불공변 때문에 오류가 뜬다.

이럴때 한정적 와일드카드 타입을 사용하면 된다.

E 생성자 매개변수에 와일드카드 타입 적용

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

E의 하위 타입의 Iterable이라는 뜻이다.

popAll도 비슷하다.

와일드카드 타입을 사용하지 않은 popAll 메서드 - 결함 존재

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

컴파일은 문제 없어 보인다.

Stack<Number> numberStack = new Stack<>();
Collection<Object> objets = ... ;
numberStack.popAll(objects);

위 코드를 실행하면 "Collection<Object>는 Collection의 하위 타입이 아니다" 라는 오류가 발생한다.

이번에도 와일드카드로 해결이 가능한데, popAll의 입력 매개변수의 탕비이 "E의 Collection"이 아니라 "E의 상위 타입의 Collection"이어야 한다.

E consumer

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

유연성을 극대화하려면 원소의 생성자나 소비자용 입력 매개변수에 와일드카드 타입을 사용하라.

PECS: Producer-Extends, Consumer-Super

한편 입력 매개변수가 생성자와 소비자의 역할을 동시에 한다면 와일드카드 타입을 써도 좋을게 없다. 이때는 타입을 정확히 지정해야하므로 와일드카드가 적절하지 않다.

와일드카드가 잘 사용되면 받아들여야 할 매개변수를 받고 거절해야 할 매개변수는 거절하는 작업이 알아서 이뤄진다. 클래스 사용자가 와일드카드 타입을 신경 써야 한다면 그 API에 문제가 있을 가능성이 크다.

매개변수와 인수의 차이
매개변수(paramter)는 메서드 선언에 정의한 변수
인수는 메서드 호출 시 넘기는 '실젯값'

타입 매개변수와 와일드카드에는 공통되는 부분이 있어서, 메서드를 정의할 때 선택하기 어려울 때가 있다.

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

어떤게 더 나을까? public API라면 간단한 두 번째가 낫다. 어떤 리스트든 이 메서드에 넘기면 명시한 인덱스의 원소들을 교환해준다.

메서드 선언에 타입 매개변수가 한 번만 나오면 와일드카드로 대체하라.
비한정적 타입 매개변수라면 비한정적 와일드카드로 바꾸고, 한정적 타입 매개변수라면 한정적 와일드카드로 바꾸면 된다.

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

위 코드는 컴파일하면 오류가 나온다.
문제는 리스트의 타입이 List<?>인데, List<?>에는 null 외에는 어떤 값도 넣을 수 없다는데 있따. 형변환이나 로타입을 사용하지 않고도 해결할 수 있다.

와일드카드 타입의 실제 타입을 알려주는 메서드를 private 도우미 메서드로 따로 작성하여 활용하는 방법이다.

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)));
}

swapHelper 메서드는 리스트가 List<E>임을 알고 있는 것이다.

0개의 댓글