Book/Effective Java

[Item 31] 한정적 와일드카드를 사용해 API 유연성을 높이라

kkkkkkkkkkkk 2022. 8. 31. 01:14

 

제네릭 타입 즉, 매개변수화 타입은 불공변(invariant)이다. 아이템 28에서 살펴봤을 것이다.

 

하지만 불공변 방식보다 유연한 무언가가 필요하다.

 

아래와 같은 스택 구조가 있다고 가정해보자.

 

public class Stack<E> {
    
    private Object[] objects;
    private int index;
    private int top = -1;

    public Stack(int size) {
        this.objects = new Object[size];
    }

    public void push(E e) {
        this.objects[index] = e;
        index++;
    }
    
    @SuppressWarnings("unchecked")
    public E pop() {
        E element = (E) this.objects[index];
        --index;
        return element;
    }
    
    public void pushAll(Iterable<E> src) {
        for (E e : src) {
            push(e);
        }
    }
}

 

이러한 구조는 깨끗히 컴파일이 되지 않지만 Iterable<E> 원소 타입이 스택의 원소 타입과 일치하면 잘 작동한다.

 

하지만 Stack<Number>로 선언한 후 pushAll(intVal) 을 호출하면 어떻게 될까?

 

매개변수화 타입은 불공변이여서 컴파일 에러가 발생한다.

 

public static void main(String[] args) {
    Stack<Number> stack = new Stack<>(10);
    Iterable<Integer> integers = new ArrayList<>();

    stack.pushAll(integers);// 컴파일 에러
}

 

컴파일 에러를 해결 하려면 한정적 와일드카드 타입을 제공해야 한다.

 

 

생산자(producer) 매개변수에 와일드카드 적용

 

값을 제공하여 저장하는 행위여서 extends 키워드를 사용한다.

 

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

 

컴파일 에러가 깔끔히 제거가 되어 타입에 안정성을 보장해준다.

 

 

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

 

 

다음으로 popAll(Collection<E> collection) 메서드를 살펴보자.

 

 

소비자 매개변수에 와일드카드 타입 적용

 

값을 사용하는 입장으로서 super 키워드를 사용한다.

 

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

 

이것 또한 컴파일 에러가 날 것이다.

 

이 문제는 super 키워드를 사용하여 풀어나가보자.

 

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

 

 

주의

반환 타입에는 한정적 와일드카드 타입을 사용하면 안 된다.

 

유연성을 높여주지 않고 클라이언트에서도 한정적 와일드카드 타입을 써야 하기 때문에 문제가 있다.

 

Comparable<E>를 사용하여 확장하는 경우 한정적 와일드카드 타입을 사용하는 편이 낫다.

 

public static <E extends Comparable<? super E>> E max(List<E> list)

 

Comparable은 원소를 사용하는 입장이므로 super키워드를 사용한다.

 

 

메서드 선언에 타입 매개변수가 한 번만 나오면 와일드 카드로 대체하라

swap 하는 메서드를 살펴보자

 

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

 

이 코드는 컴파일 에러가 날 것이다. 이유는 <?> 타입이 null 외에는 어떤 값도 넣을 수 없어서 에러가 발생한다.

코드를 개선해보자.

 

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

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

 

private 도우미 메서드를 생성하고 <E> 타입으로 만든다.

 

이 List<E> 에서 꺼낸 원소는 항상 E 이고, E 타입의 값이라면 이 리스트에 넣어도 안전함을 할고 있다.

 

 

정리

  • 조금 복잡하더라도 와일드카드 타입을 적용하면 API가 훨씬 유연해진다.
  • 생산자는 extends 소비자는 super.
  • Comparable과 Comparator는 모두 소비자다.