jeonyoungho Aug 22, 2022 2022-08-22T00:00:00+09:00
Aug 26, 2022 2022-08-26T14:04:09+09:0017 min
이전 아이템28에서 이야기했듯 매개변수화 타입은 불공변(invariant)이다. 이를 꼭 기억하자.
즉 서로 다른 타입 Type1과 Type2가 있을 때 List<Type1>은 List<Type2>의 하위 타입도 상위 타입도 아니다.
예를 들어 List<Object>에는 어떤 객체든 넣을 수 있지만 List<String>에는 문자열만 넣을 수 있다. 즉, List<String>은 List<Object>가 하는 일을 제대로 수행하지 못하니 하위 타입이 될 수 없다(리스코프 치환 원칙에 어긋난다. 아이템10 참조)
이전에 언급되었던 Stack 예제에서 일련의 원소를 스택에 넣는 public API(pushAll 메서드)를 추가한다고 해보자.
StackTest.java:7: error: incompatible types: Iterable
cannot be converted to Iterable<Number>
Stack.pushAll(integers);
⌃
다행히 해결책은 있다. 자바는 이런 상황에 대처할 수 있는 한정적 와일드카드 타입이라는 특별한 매개변수화 타입을 지원한다.
pushAll의 입력 매개변수 타입은 E의 Iteralbe이 아니라 E의 하위 타입의 Iterable이어야 하며, 와일드 카드 타입 Iterable<? extends E>가 정확히 이런 뜻이다.
이를 와일드카드 타입을 사용하도록 puahAll 메서드를 수정하면 아래와 같다.
1
2
3
4
5
6
// 코드 31-2 E 생산자(producer) 매개변수에 와일드카드 타입 적용 (182쪽)publicvoidpushAll(Iterable<?extendsE>src){for(Ee:src){push(e);}}
이번에는 pushAll과 짝을 이루는 popAll 메서드를 작성해보자.
popAll 메서드는 Stack 안의 모든 원소를 주어진 컬렉션으로 옮겨 담는다.
아래처럼 작성했다고 생각해보자.
1
2
3
4
5
// 코드 31-3 와일드카드 타입을 사용하지 않은 popAll 메서드 - 결함이 있다! (183쪽)publicvoidpopAll(Collection<E>dst){while(!isEmpty())dst.add(pop());}
이번에도 주어진 컬렉션의 원소 타입이 스택의 원소 타입과 일치한다면 말끔히 컴파일되고 문제없이 작동된다.
하지만 이번에도 역시나 완벽하진 않다. Stack의 원소를 Object용 컬렉션으로 옮기려 한다 했을때 "Collection
이번에도 와일드카드 타입으로 해결할 수 있다.
popAll의 입력 매개변수의 타입이 ‘E의 Collection이 아니라 E의 상위 타입의 Collection’ 이어야 한다.
1
2
3
4
5
// 코드 31-2 E 생산자(producer) 매개변수에 와일드카드 타입 적용 (182쪽)publicvoidpushAll(Iterable<?extendsE>src){for(Ee:src)push(e);}
메시지는 분명하다. 유연성을 극대화하려면 원소의 생산자나 소비자용 입력 매개변수에 와일드카드 타입을 사용해라!
한편, 입력 매개변수가 생산자와 소비자 역할을 동시에 한다면 와일드카드 타입을 써도 좋을 게 없다.
타입을 정확히 지정해야 하는 상황으로, 이때는 와일드카드 타입을 쓰지 말아야 한다.
팩스(PECS) 원칙: producer-extends, consumer-super
즉, 매개변수 타입 T가 생산자라면 <? extends T>를 사용하고, 소비자라면 <? super T>를 사용해라.
Stack 예에서 pushAll의 src 매개변수는 Stack이 사용할 E 인스턴스를 생산하므로 src의 적절한 타입은 Iterable<? extends E>이다. 한편, popAll의 dst 매개변수는 Stack으로부터 E 인스턴스를 소비하므로 dst이 적절한 타입은 Collection<? super E>이다.
PECS 원칙은 와일드 카드 타입을 사용하는 기본 원칙이다.
팩스 원칙을 적용한 예시
1) 아이템28의 Chooser 클래스
Chooser 생성자는 아래와 같이 선언했다.
1
publicChollser(Collection<T>choices)
이 생성자로 넘겨지는 choices 컬렉션은 T 타입의 값을 생산 하기만 하니 T를 확장하는 와일드 카드 타입을 사용해 선언해야 한다.
이를 수정한 모습은 아래와 같다.
1
2
3
4
// 코드 31-5 T 생산자 매개변수에 와일드카드 타입 적용 (184쪽)publicChooser(Collection<?extendsT>choices){choiceList=newArrayList<>(choices);}
Chooser<Number>의 생성자에 List<Integer>를 넘기고 싶다고 해보자.
수정 전 생성자에선 컴파일조차 되지 않지만, 수정 후 생성자에선 문제가 전혀 되지 않는다.
원인은 리스트의 타입이 List<?>인데, List<?>에는 null 외에는 어떤 값도 넣을 수 없다는 데 있다.
다행히 (런타임 오류를 낼 가능성이 있는) 형변환이나 리스트의 로 타입을 사용하지 않고도 해결할 길이 있다. 바로 와일드카드 타입의 실제 타입을 알려주는 메서드를 private 도우미 메서드로 따로 작성하여 활용하는 방법이다.
실제 타입을 알아내려면 이 도우미 메서드는 제네릭 메서드여야 한다. 아래 코드를 보자.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
publicstaticvoidswap(List<?>list,inti,intj){swapHelper(list,i,j);}// 와일드카드 타입을 실제 타입으로 바꿔주는 private 도우미 메서드privatestatic<E>voidswapHelper(List<E>list,inti,intj){list.set(i,list.set(j,list.get(i)));}publicstaticvoidmain(String[]args){// 첫 번째와 마지막 인수를 스왑한 후 결과 리스트를 출력한다.List<String>argList=Arrays.asList(args);swap(argList,0,argList.size()-1);System.out.println(argList);}
swapHelper 메서드는 리스트가 List 임을 알고 있다. 즉, 이 리스트에서 꺼낸 값의 타입은 항상 E이고, E타입의 값이라면 이 리스트에 넣어도 안전함을 알고 있다. 그러기에 정상적으로 문제 없이 동작하게 된다.
1
조금복잡하더라도와일드카드타입을
핵심 정리: 조금 복잡하더라도 와일드카드 타입을 적용하면 API가 훨씬 유연해진다. 그러니 널리 쓰일 라이브러리엔 반드시 와일드카드 타입을 적절히 사용해줘야 한다. 생산자에는 extends 소비자에는 super를 사용하는 PECS를 기억하자. 추가적으로 Comparable과 Comparator는 모두 소비자라는 사실도 잊지 말자.