-
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))); } }
'독서 > Effective Java' 카테고리의 다른 글
Item 35: Use instance fields instead of ordinals (1) 2022.01.08 Item 30: Favor generic methods (0) 2022.01.02 Item 29: Favor generic types (0) 2022.01.02 Item 28: Prefer lists to arrays (0) 2022.01.02 Item 27: Eliminate unchecked warnings (0) 2022.01.02