Skip to content

Latest commit

 

History

History
194 lines (129 loc) · 5.63 KB

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

File metadata and controls

194 lines (129 loc) · 5.63 KB

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

class Storage<E> { 
    private List<E> list = new ArrayList<>();

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

    private void push(E e){
        list.add(e);
    }
}

위의 코드는 컴파일이 되긴 하지만, 결함이 존재하고 있는데요.

Storage로 객체를 생성하고 pushAll 메서드의 매개변수로 List를 넣어주면, 어떻게 될까요?

Storage<Number> storage = new Storage<>();
List<Integer> integers = new ArrayList<>();

storage.pushAll(integers);  // Compile Error!

이는 제네릭이 불공변이기 때문에 컴파일 오류가 뜨게 됩니다.

여기서 불공변이란 Integer가 Number의 하위 타입이어도, List과 List는 아무런 관계가 없다는 것입니다.


이 상황을 해결할 수 있는 것이 바로 한정적 와일드 카드입니다.

Collection<? extends E>

위의 의미는 ‘E의 하위 타입의 Collection’이라는 뜻입니다.

Collection<? super E>

위의 의미는 ‘E의 상위 타입의 Collection’이라는 뜻입니다.

위의 pushAll 메소드를 아래의 코드로 변경해봅시다.

public void pushAll(Collection<? extends E> src) {
    for(E e : src){
        push(e);
    }
}
Storage<Number> storage = new Storage<>();
List<Integer> integers = new ArrayList<>();

storage.pushAll(integers);  // OK!

위에서 발생한 컴파일 에러가 없어지는 것을 확인해 볼 수 있습니다.

Collection<? extends E>를 통해 입력 매개변수 타입으로 Number 자기 자신과 하위 타입이 들어올 수 있도록 하였기 때문입니다.

이번에는 list에 있는 값들을 매개변수로 주어진 컬렉션에 옮겨 담는 코드를 작성해 봅시다.

public void popAll(Collection<E> dst) {
    dst.addAll(list);
    list.clear();
}

위의 코드 또한 컴파일이 되긴 하지만, 결함이 존재하고 있습니다.

Storage<Number> storage = new Storage<>();
storage.pushAll(Arrays.asList(1,2,3,4));

List<Object> objects = new ArrayList<>();
storage.popAll(objects);  //Compile Error!

List를 넣으면 컴파일 오류가 뜨게됩니다.

이번에는 위의 코드를 아래와 같이 변경해 보겠습니다.

public void popAll(Collection<? super E> dst) {
    dst.addAll(list);
    list.clear();
}
Storage<Number> storage = new Storage<>();
storage.pushAll(Arrays.asList(1,2,3,4));

List<Object> objects = new ArrayList<>();
storage.popAll(objects);  //Ok!

위에서 발생한 컴파일 오류가 없어지는 것을 확인해 볼 수 있습니다.

입력 매개변수 타입으로 Number 자기 자신과 상위 타입이 들어올 수 있도록 하였기 때문입니다.


PECS

여기서 펙스(PECS) : producer-extends, consumer-super라는 개념을 확인해 볼 수 있는데요.

위에서 살펴본 pushAll에서는 src 매개변수는 Storage에서 사용할 E 인스턴스에 값을 생성하고 있기 때문에 상한 제한을 Collection<? extends E> 사용하는 것입니다.

반면에 popAll에서는 dst 매개변수는 Storage의 E 인스턴스를 소비하므로 Collection<? super E>를 사용하는 것이 적절합니다.

→ 유연성을 극대화하기 위해, 원소의 생성자나 소비자용 입력 매개변수에 와일드 카드 타입을 사용합시다!

아이템 28, 코드 30-2

public static <E> Set<E> union(Set<E> s1, Set<E> s2) {
        Set<E> result = new HashSet<>(s1);
        result.addAll(s2);    
        return result;
				// result를 제공한다.
}

public static Set union(Set s1, Set s2)

→ public static Set union(Set<? extends E> s1, Set<? extends E> s2)

Set<Integer> integers = Set.of(1, 2, 3);
Set<Double> doubles = Set.of(1.0, 2.0, 3.0);
Set<Number> numbers = union(doubleSet, integerSet)  // 자바 8부터 가능
Set<Number> numbers = Union.<Number>union(integers, doubles);  // 자바 7까지

타입 매개변수 VS 와일드 카드

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

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

메서드를 정의할 때, 둘 중 어느 것을 사용해도 괜찮은 경우가 많다고 합니다.

책에서는 메서드 선언에 타입 매개변수가 한 번만 나오면 와일드 카드를 사용하라고 명시되어 있는데요.

따라서 타입 매개변수가 한 개인 swap 메서드를 와일드 카드로 선언해 보겠습니다.

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

그런데 이 코드는 컴파일하면 오류가 뜨게 됩니다.

이유는 List<?>에는 null 이외의 어떤 값도 넣을 수 없기 때문입니다. 이 경우에는 swapHelper라는 도우미 메서드를 통해 해당 문제를 해결할 수 있습니다.

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

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

→ 클라이언트는 복잡한 swapHelper의 기능을 모른 채, swap 메서드를 호출할 수 있다.

저자의 결론:

한정적 와일드 카드를 적용하여 유연한 API를 설계해 보자!


Reference

조슈아 블로크, 이펙티브 자바 Effective Java 3/E, 2018

https://jjingho.tistory.com/76

https://madplay.github.io/post/use-bounded-wildcards-to-increase-api-flexibility