ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Item 31: Use bounded wildcards to increase API flexibility
    독서/Effective Java 2022. 1. 7. 23:12

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

    Parameterized Type은 불공변이다.

    Type1과 Type2가 있을 때 List<Type1>은 List<Type2>의 하위 타입, 상위 타입이 아니다.

    Ex) String은 Object의 하위 타입이지만, List<String>은 List<Object>의 하위 타입이 아니다.

    List<String>은 List<Object>가 하는 일을 제대로 수행하지 못하니 하위 타입이 될 수 없다(리스코프 치환 원칙에 어긋남. item 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);
        }
      }
    }
    

    정상적으로 컴파일은 이루어지나, 아래의 테스트는 통과하지 못한다.

    @Test
    void stackPushAllTest(){
    	Stack<Number>stack = new Stack<>();
    	List<Integer>ints = new ArrayList<>();
      ints.add(1);
    	Iterable<Integer>integers = ints;
    	// 애초에 컴파일 에러가 떨어지게 된다.
      stack.pushAll(integers);
    }
    

    해당 ints List를 List<Number>로 바꾼 뒤 해당 테스트를 수행하면 정상적으로 통과할 것이다.

    하지만, 기대했던 결과는 List<Integer>건 List<Number>건 결국 Integer는 Number의 하위 타입이니 두 타입 모두 stack.pushAll이 수행되는 것이다.

    이러한 상황을 해결해줄 수 있는 것이 한정적 와일드카드이다.

    한정적 와일드카드

    pushAll을 다음과 같이 수정하고 테스트를 돌려보자.

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

    정상적으로 테스트에 통과하는 것을 확인할 수 있다.

    그렇다면 이유가 뭘까?

    <? extends E>는 E의 하위 타입을 의미한다. Iterable<? extends E>는 E의 하위 타입의 Iterable을 의미하고 그렇기에 Stack<Number>에서 Iterable<Number>, Iterable<Integer>를 모두 지원하는 것이다.

    위의 수정 사항으로 인해 Number의 하위 타입을 지원하는 안전한 메서드가 되었음을 확인할 수 있다.

    반대의 경우로 popAll을 생성해보자.

    public void popAll(Collection<E>dst) {
    	while(!isEmpty() ){
    		dst.add(pop());
    	}
    }
    
    @Test
    void stackPopAllTest() {
      Stack<Number> numbers = new Stack<>();
      List<Object> objects = new ArrayList<>();
      numbers.push(1);
      numbers.push(2);
      numbers.popAll(objects);
    }
    

    위의 pushAll과 마찬가지로 해당 메서드도 Stack<Number> → Collection<Object>의 연결이 불가능하다.

    이번에도 한정적 와일드카드 타입으로 변경해서 해결해보자.

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

    이번에는 extends가 들어갈 자리에 super를 삽입해서 넣었다. Stack<Number> → Collection<Object>로 이루어지기 때문에 상위 타입까지 포함할 수 있어야 하기 때문이다.

    pushAll을 해당 데이터 타입에 생성, popAll을 해당 데이터 타입을 사용이라는 관점으로 봤을 때 다음과 같이 이해를 하고 넘어가면 좋을 듯 하다.

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

    입력 매개변수가 producer와 consumer의 역할을 동시에 한다면, 한정적 와일드카드를 사용하지말고 특정 타입으로 사용될 수 있도록 제한하자.

    또한, 반환 타입은 한정적 와일드카드를 사용하지 말자.

    추가 예제

    // 변경 전 item 28의 chooser
    public Chooser(Collection<T> choices)
    // 변경 후
    public Chooser(Collection<? extends T> choices)
    
    // 변경 전 item 30의 union
    public static <E> Set<E> union(Set<E> s1, Set<E> s2)
    // 변경 후, parameter에 모두 한정적 와일드카드 적용됨
    public static <E> Set<E> union(Set<? extends E> s1, Set<? extends E> s2)
    

    Parameter vs Argument

    Method에서의 입력 매개변수 vs 외부에서 Method를 사용할 때 들어가는 실제 값

    위의 popAll에서 Collection<E>dst 는 입력 매개변수이고, numbers.popAll(objects); 에서의 objects가 Argument이다.

    응용

    이번엔 item 30의 max 메서드를 확인해보자.

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

    다음을 와일드카드 타입을 사용해 다듬은 선언 부분은 다음과 같다.

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

    해당 메서드에는 PECS 공식이 두 번 적용되어 있다.

    • 입력 매개변수: E 인스턴스를 생산하므로 원래의 List<E> → List<? extends E>로 수정
    • 타입 매개변수: Comparable은 E 인스턴스를 소비하기 때문에 Comparable<E> → Comparable<? super E>로 대체
      • Comparable은 언제나 Consumer이기 때문에, 일반적으로는 Comparable<E>보다는 Comparable<? super E>를 사용하는 것이 더 낫다(Comparator도 동일)

    타입 매개변수와 와일드카드 중 선택해야 하는 경우

    타입 매개변수와 와일드카드에는 공통되는 부분이 있어서, 메서드를 정의할 때 둘 중 어느 것을 사용해도 괜찮을 때가 많다.

    Ex) 주어진 리스트에서 명시한 두 인덱스의 원소들을 교환(swap)하는 정적 메서드

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

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

    기본 규칙

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

    하지만 두 번째 swap 선언에는 문제가 하나 있는데, 해당 코드가 컴파일되지 않음.

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

    원인: 리스트의 타입이 List<?>인데, List<?>에는 null 이외에는 어떤 값도 넣을 수 없다.

    해결책: 와일드카드 타입의 실제 타입을 알려주는 메서드를 private Helper 메서드로 따로 작성

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

    댓글

Designed by Tistory.