ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Java] 한정적 와일드 카드 (Effective Java 3rd_Item31)
    Java 2019. 8. 11. 23:51
    반응형

     한정적 와일드카드를 이용하면 매개변수화 타입을 이용하는 방법보다 더 유연한 타입을 만들 수 있습니다. 매개변수화 타입은 불공변 타입이므로 명시된 한정자가 없다면 타입간에 불일치가 발생하여 에러가 생길 수 있습니다.

     

     아래와 같이 스택 클래스를 예로 보겠습니다.

    public class StackGeneric<E> {
        private E[] elements;
        private int size = 0;
        private static final int DEFAULT_INITIAL_CAPACITY = 16;
    
        @SuppressWarnings("unchecked")
        public StackGeneric() {
            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);
            }
        }
    
        public void pushAll(Iterable<E> src) {
            for (E e : src) {
                push(e);
            }
        }
    }

      위와 같이 정의된 스택에서 타입 매개변수로는 Number를 지정하고, pushAll() 메서드를 호출할 때는 Iterater<Integer> 타입을 파라미터로 주입한다면 다음과 같이 오류가 발생합니다. 이는 매개변수화 타입이 불공변이기 때문에 발생하는 것입니다.

     이러한 오류를 해결하기 위해서 한정적 와일드카드 타입을 이용할 수 있습니다. 한정적 와일드카드의 선언은 아래와 같이 할 수 있습니다.

    <? extends E>

     여기서 '?' 는 와일드카드이며, 타입매개변수로 어떤 타입이든 지정될 수 있다는 뜻입니다.

    그리고 이어서 나오는 'extends E' 는 앞의 와일드 카드가 타입 매개변수 E 를 확장해야 한다는 뜻입니다.

     

     한정적 와일드카드를 적용하여 방금 보았던 스택의 pushAll 메서드에 적용시켜보면 아래와 같이 변경할 수 있습니다.

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

     그리고 변경한 코드를 바탕으로 다시 테스트 코드를 확인하면 에러가 사라진 것을 확인할 수 있습니다. 이는 Integer 타입이 Number 타입을 확장하고 있기 때문입니다.

     


     

     반대로 타입 매개변수가 Number인 Stack에서 모든 원소를 List<Object>로 옮기는 메서드가 아래와 같이 있을때, 아까 보았던 이유와 마찬가지로 불공변 타입이기 때문에 다음과 같이 에러가 발생합니다.

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

     

     이번에도 한정적 와일드카드를 이용하면 문제를 해결할 수 있습니다. 이번에는 extends E 가 아니라 E의 상위 타입이어야 한다는 super E 로 선언하였습니다.

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

     


     

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

     

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

     위의 공식은 매개변수화 타입 T가 생산자라면 <? extends T>를 사용하고, 소비자라면 <? super T>를 사용하라는 의미입니다. Stack의 예에서 pushAll() 메서드의 매개변수는 Stack이 사용할 T 인스턴스를 생산하므로 extends를 사용하고, popAll() 메서드는 Stack으로부터 E d인스턴스를 소비하므로 super를 사용한 것입니다.

     


     

     이번엔 조금 더 복잡한 케이스를 살펴보겠습니다.

     

    Delayed는 Comparable 인터페이스를 확장하였고, 자신의 우선순위를 반환하는 getPriority() 메서드를 가지고 있습니다.

    public interface Delayed extends Comparable<Delayed> {
        int getPriority();
    }

     

     이어서 ScheduledFuture는 위에서 선언한 Delayed 인터페이스를 확장하고 있고, work() 메서드를 가지고 있습니다.

    public interface ScheduledFuture extends Delayed {
        void work();
    }

     

      ScheduledFuture 의 구현체로 Schedule 이라는 클래스가 아래와 같고, 타입 매개변수로 Work라는 타입을 확장하는 V가 지정되어 있습니다.

    priority = 스케쥴의 우선 순위

    V job = 해야할 일의 인스턴스 참조

    work()는 Work를 확장한 참조 객체 V를 실행시키는 역할을 합니다.

    public class Schedule<V extends Work> implements ScheduledFuture {
    
        private int priority;
        private V job;
    
        public Schedule(int priority, V job) {
            this.priority = priority;
            this.job = job;
        }
    
        @Override
        public void work() {
            job.running();
        }
    
        @Override
        public int getPriority() {
            return this.priority;
        }
    
        @Override
        public int compareTo(Delayed o) {
            return this.getPriority() - o.getPriority();
        }
    }

     

     위에서 등장한 Work 인터페이스는 단순하게 running() 메서드를 가지고 있습니다.

    public interface Work {
        void running();
    }

     

    Work 인터페이스를 구현한 Job 클래스입니다. jobName이라는 필드값을 가지고 있으며, 할 일의 이름을 나타냅니다.

    public class Job implements Work {
    
        private String jobName;
    
        public Job(String jobName) {
            this.jobName = jobName;
        }
    
        @Override
        public void running() {
            System.out.println("working..." + jobName);
        }
    }
    

     

     위 코드들을 클래스 다이어그램으로 나타내면 아래와 같습니다.

     

    그리고 Schedule 리스트에서 우선순위가 가장 높은(priority 값이 가장 낮은) 스케쥴을 반환해주는 제네릭 메서드가 아래와 같이 있습니다.

        public static <E extends Comparable<? super E>> E max(List<? extends E> list) {
            if (list.isEmpty()) {
                throw new IllegalArgumentException("Collection is empty");
            }
    
            E result = null;
            for (E e : list) {
                if (result == null || result.compareTo(e) > 0) {
                    result = Objects.requireNonNull(e);
                }
            }
            return result;
        }

     입력에 대한 매개변수는 E 인스턴스를 생산하므로 extends E 를 사용하였습니다.

    그 다음은 조금 더 복잡한 타입 매개변수입니다. Comparable 인터페이스는 E 인스턴스를 소비하여 우선순위 관계를 뜻하는 정수를 생산하므로 Comparable<? super E> 를 확장하는 E 를 타입 매개변수로 지정하였습니다. Comparable은 언제나 소비자이므로, 일반적으로 Comparable<E> 보다는 Comparable<? super E>로 사용하는 편이 낫습니다.

     

     위의 메서드를 바탕으로 테스트 코드를 만들어서 실행해보면 가장 우선순위가 높은 job을 실행시키는 결과를 확인할 수 있습니다.

        @Test
        public void 와일드카드제네릭_max_2() {
            Work job1 = new Job("코딩하기");
            Work job2 = new Job("스터디 가기");
            Work job3 = new Job("세미나 가기");
    
            List<ScheduledFuture> scheduledFutures = new ArrayList<>();
            scheduledFutures.add(new Schedule<>(0, job1));
            scheduledFutures.add(new Schedule<>(5, job2));
            scheduledFutures.add(new Schedule<>(3, job3));
    
            ScheduledFuture scheduledFuture = WildCardGeneric.max(scheduledFutures);
            scheduledFuture.work();
        }

     


     

     만약 한정적 와일드 카드를 사용하지 않고, max가 아래와 같은 선언으로 되어있었다면 위의 테스트 코드는 에러가 발생할 것입니다.

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

     왜냐하면 ScheduleFuture 인터페이스가 Comparable<ScheduledFuture>를 구현하지 않았기 때문입니다. Comparable은 Delayed 인터페이스가 확장하고 있습니다. 따라서 불공변인 타입 매개변수는 max() 메서드에서 정확히 E 타입과 일치하지 않기 때문에 에러가 발생하는 것입니다. 즉, Comparable을 직접 구현하지 않고, 직접 구현한 다른 타입을 확장한 타입을 지원하기 위해서는 와일드카드 타입을 이용해야 합니다.


     

    참고자료

    Effective Java 3rd (인사이트 출판)

    반응형

    댓글

Designed by Tistory.