ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Item 31. 한정적 와일드카드를 사용해 API 유연성을 높이라
    Book/Effective Java 3E 2022. 11. 19. 10:15
    반응형

      Item 29의 Stack 클래스에 일련의 원소를 스택에 넣는 pushAll 메서드와 짝을 이루는 popAll 메서드를 추가해야 한다고 해봅시다.

    package effectivejava.chapter5.item31;
    import java.util.*;
    
    // 와일드카드 타입을 이용해 대량 작업을 수행하는 메서드를 포함한 제네릭 스택
    public class Stack<E> {
        private E[] elements;
        private int size = 0;
        private static final int DEFAULT_INITIAL_CAPACITY = 16;
    
        // 코드 29-3 배열을 사용한 코드를 제네릭으로 만드는 방법 1
        // 배열 elements는 push(E)로 넘어온 E 인스턴스만 담는다.
        // 따라서 타입 안전성을 보장하지만,
        // 이 배열의 런타임 타입은 E[]가 아닌 Object[]다!
        @SuppressWarnings("unchecked") 
            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();
            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);
        }
    
    //    // 코드 31-1 와일드카드 타입을 사용하지 않은 pushAll 메서드 - 결함이 있다!
    //    public void pushAll(Iterable<E> src) {
    //        for (E e : src)
    //            push(e);
    //    }
    
         // 코드 31-2 E 생산자(producer) 매개변수에 와일드카드 타입 적용
        public void pushAll(Iterable<? extends E> src) {
            for (E e : src)
                push(e);
        }
    
    //    // 코드 31-3 와일드카드 타입을 사용하지 않은 popAll 메서드 - 결함이 있다!
    //    public void popAll(Collection<E> dst) {
    //        while (!isEmpty())
    //            dst.add(pop());
    //    }
    
        // 코드 31-4 E 소비자(consumer) 매개변수에 와일드카드 타입 적용
        public void popAll(Collection<? super E> dst) {
            while (!isEmpty())
                dst.add(pop());
        }
    
        // 제네릭 Stack을 사용하는 맛보기 프로그램
        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);
    
            Collection<Object> objects = new ArrayList<>();
            numberStack.popAll(objects);
    
            System.out.println(objects);
        }
    }

      코드 31-1와 코드 31-3 메서드는 컴파일되지만 완벽하지 않습니다.

     

      코드 31-1의 경우, Iterable src의 원소 타입이 스택 원소 타입과 일치하면 잘 동작합니다. 하지만 다음과 같은 상황에서는 오류가 발생합니다. 매개변수화 타입이 불공변이기 때문입니다. 코드 31-2와 같이 와일드카드 타입을 사용하도록 수정하면 Stack은 물론 이를 사용하는 클라이언트 코드도 말끔히 컴파일됩니다.

    Stack<Number> numberStack = new Stack<>();
    iterable<Integer> integers = ...;
    numberStack.pushAll(integers);
    
    // Error Message
    StackTest.java:7: error: incompatible types: Iterable<Integer>
    cannot be converted to Iterable<Number>
            numberStack.pushAll(integers);

     

      코드 31-3의 경우도 주어진 컬렉션의 원소 타입이 스택의 원소 타입과 일치한다면 문제없이 동작하지만, 다음과 같은 상황에서는 코드 31-1의 경우와 비슷한 오류가 발생합니다. 코드 31-4와 같이 와일드카드 타입을 사용하도록 수정하면 Stack과 클라이언트 코드 모두 말끔히 컴파일됩니다.

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

     

    Note. 여기서 입력 매개변수를 생산자(producer)라 한 것은 입력 매개변수로부터 이 컬렉션으로 원소를 옮겨 담는다는 뜻이다. 반대로 코드 31-4처럼 이 컬렉션 인스턴스의 원소를 입력 매개변수로 옮겨 담는다면 그 매개변수를 소비자(consumer)라 한다.

     

      유연성을 극대화하려면 원소의 생산자나 소비자용 입력 매개변수에 와일드카드 타입을 사용합니다. 입력 매개변수가 생산자와 소비자 역할을 동시에 한다면 와일드카드 타입을 써도 좋을 게 없습니다. 타입을 정확히 지정해야 하는 상황으로, 이때는 와일드카드 타입을 쓰지 말아야 합니다.

     

    와일드카드 타입을 사용하는 기본 원칙

    펙스(PECS): producer-extends, consumer-super

      Stack 예에서 pushAll의 src 매개변수는 Stack이 사용할 E 인스턴스를 생산하므로 src의 적절한 타입은 Iterable<? extends E>입니다. 한편, popAll의 dst 매개변수는 Stack으로부터 E 인스턴스를 소비하므로 dst의 적절한 타입은 Collection<? super E>입니다. 나프탈린(Naftalin)과 와들러(Wadler)는 이를 겟풋 원칙(Get and Put Principle)으로 부릅니다.[Naftalin07, 2.4]

     

      이 공식을 Item 28의 Chooser 생성자, 코드 30-2의 union 메서드 및 코드 30-7의 max 메서드에 적용한 코드는 다음을 참고하시면 됩니다.

    https://github.com/HanseomKim/effective-java-3e-source-code/tree/master/src/effectivejava/chapter5/item31

     

    GitHub - HanseomKim/effective-java-3e-source-code: 『이펙티브 자바, 3판』(인사이트, 2018)

    『이펙티브 자바, 3판』(인사이트, 2018). Contribute to HanseomKim/effective-java-3e-source-code development by creating an account on GitHub.

    github.com

    Note. 반환 타입에는 한정적 와일드카드 타입을 사용하면 안 된다. 유연성을 높여주기는커녕 클라이언트 코드에서도 와일드카드 타입을 써야 하기 때문이다.

     

    매개변수(parameter) vs 인수(argument)

      매개변수는 메서드 선언에 정의한 변수이고, 인수는 메서드 호출 시 넘기는 '실젯값'입니다.

    void add(int value) { ... } // value: 매개변수
    add(10)                     // 10: 인수
    
    class Set<T> { ... }        // T: 타입 매개변수
    Set<Integer> = ...;         // Integer: 타입 인수

     

    조금 복잡하더라도 와일드카드 타입을 적용하면 API가 훨씬 유연해진다.

    그러니 널리 쓰일 라이브러리를 작성한다면 반드시 와일드카드 타입을 적절히 사용해줘야 한다.

    PECS 공식을 기억하자.

    즉, 생산자(producer)는 extends를 소비자(consumer)는 super를 사용한다.

    Comparable과 Comparator는 모두 소비자라는 사실도 잊지 말자.

     

    [참고 정보]

    이펙티브 자바 Effective Java 3/E 도서 [조슈아 블로크 ]

    이펙티브 자바 깃허브 저장소

    <이펙티브 자바, 3판> 번역 용어 해설

    반응형

    댓글

Designed by Tistory.