[이펙티브 자바] 아이템31 | 한정적 와일드카드를 사용해 API 유연성을 높이라

제롬·2022년 3월 25일
0

불공변인 매개변수화 타입

서로 다른 Type1Type2가 있을 때 List<Type1>List<Type2>는 하위 타입도 상위 타입의 관계도 아니다. 예컨데, List<String>List<Object>의 하위 타입이 아니라는 의미이다. List<String>에는 문자열만 넣을 수 있지만 List<Object>는 어떤 객체든 넣을 수 있다.

List<String>List<Object>가 하는 일을 제대로 수행하지 못하니 (리스코프 치환 원칙을 위반) 하위 타입이 될 수 없다.

하지만 때론 불공변 방식보다 유연한 방식이 필요할 때가 있다. 이럴 때 한정적 와일드카드 타입이라는 특별한 매개변수화 타입이 유용하게 사용될 수 있다.

한정적 와일드카드 타입 - 생산자

[와일드카드를 사용하지 않은 pushAll 메서드]

...
public void pushAll(Iterable<E> src) {
    for (E e : src) {
        push(E);
    }
}
...
public static void main(String[] args) {
    Stack<Number> numberStack = new Stack<>();
    Iterable<Integer> iterable = ...;
        
    numberStack.pushAll(iterable);
}
...
  • src 매개변수는 Stack이 사용할 E인스턴스를 생산하므로 생산자라고 볼 수 있다.

  • IntegerNumber의 하위 타입이므로 논리적으로 잘 동작해야 할 것 같지만 실제로는 타입 변경할 수 없다는 에러가 발생한다.

  • 이런 상황에 한정적 와일드카드 타입이 유용하게 사용될 수 있다.

[E 생산자(producer) 매개변수에 와일드카드 타입 적용]

...
public void pushAll(Iterable<? extends E> src) {
    for (E e : src) {
        push(E);
    }
}
...
  • Iterable <? extends E>EIterable이 아니라 E의 하위 타입의 Iterable이어야 한다는 의미를 갖는다.

  • 이렇게 한정적 와일드카드를 사용하면 타입이 안전해지므로 클라이언트 코드가 정상적으로 컴파일된다.

한정적 와일드카드 타입 - 소비자

[와일드카드를 사용하지 않은 popAll 메서드]

...
public void popAll(Collection<E> dst) {
    while (!isEmpty()) {
        dst.add(pop());
    }
}
...
public static void main(String[] args) {
    Stack<Number> numberStack = new Stack<>();
    Collection<Object> objects = ...;
        
    numberStack.popAll(objects);
}
...
  • Collection<Object>Collections<Number>의 하위 타입이 아니기 때문에 오류가 발생한다. 이번 상황역시 와일드카드 타입으로 해결이 가능하다.

[E 소비자 매개변수에 와일드카드 타입 적용]

...
public void popAll(Collection<? super E> dst) {
    while (!isEmpty()) {
        dst.add(pop());
    }
}
...
  • dst 매개변수는 Stack으로부터 E 인스턴스를 소비하므로 소비자라고 볼 수 있다.

펙스(PECS) - 와일드카드 타입 사용 공식

producer - extends
consumer - super

  • 매개변수화 타입 T가 생산자(producer)라면 <? extends T>를 사용한자.
  • 매개변수화 타입 T가 소비자(consumer)라면 <? super T>를 사용하자.

[PECS 공식 사용 전]

public static <E> Set<E> union(Set<E> s1, Set<E> s2)
  • s1s2는 생산자이니 PECS 공식에 따라 생산자 와일드카드 방식에 따라 사용해야 한다.

[PECS 공식 사용 후]

public static <E> Set<E> union(Set<? extends E> s1, Set<? extends E> s2)
  • 반환타입에는 한정적 와일드 카드 타입을 사용하면 안된다. 유연성을 높여주기는 커녕 클라이언트 코드에서 와일드카드 타입을 사용해야하기 때문이다.

[클라이언트 코드]

Set<Integer> integers = Set.of(1, 3, 5);
Set<Double> doubles = Set.of(2.0, 4.0, 6.0);
Set<Number> numbers = union(integers, doubles);
  • 와일드카드가 제대로 사용된다면 사용자는 와일드 카드가 사용된지도 모른다. 만약 사용자가 와일드카드 타입을 신경써야 한다면 그 API는 문제가 있을 수 있다.

  • 참고로, 만약 자바 버전이 7버전이라면 위 클라이언트 코드에서 명시적 타입 인수를 사용해주어야 한다. 자바 8이전까지는 타입 추론 능력이 충분하지 못해 반환타입을 명시해야 했다.

[자바 7까지는 명시적 타입 인수를 사용해야 했다.]

Set<Number> numbers = Union.<Number>union(integers,doubles);

매개변수와 인수의 차이

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

[매개변수와 인수 예시]

void add(int value) {...}
add(10);

위 코드에서 value는 매개변수이고 10은 인수다.

[제네릭 매개변수와 인수 예시]

class Set<T> {...}
Set<Integer> = {...}

여기서 T는 타입 매개변수가 되고, Integer는 타입 인수가 된다.

타입매개변수와 와일드카드 모두 사용해도 괜찮을 경우

기본 규칙: 메서드 선언에 타입 매개변수가 한번만 나오면 와일드 카드로 대체하라

[swap 메서드의 두 가지 선언 - 비한정적 타입 매개변수, 비한정적 와일드 카드]

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

이때, 한정적 타입 매개변수라면 한정적 와일드카드로 비한정적 타입 매개변수라면 비한정적 와일드카드로 변경하면 된다.

[직관적으로 구현한 코드 - 문제발생]

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

이 코드는 list.get(i)로 꺼낸 코드를 다시 리스트에 넣을 수 없다는 오류를 발생시킨다. 이 오류는 제네릭 메서드인 private 도우미 메서드를 작성함으로 해결할 수 있다.

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

// 와일드카드 타입을 실제 타입으로 바꿔주는 private 도우미 메서드
private static <E> void swapHelper(List<E> list, int i, int j){
	list.set(i, list.set(j, list.get(i));
}

swapHelper 메서드는 List<E>에서 꺼낸 타입이 항상 E이고 E타입의 값은 해당 List에 다시 넣어도 안전함을 알고 있다.

정리

유연성을 극대화하려면 원소의 생산자나 소비자용 입력 매개변수에 와일드카드 타입을 사용하자. 생산자(producer)는 extends를 소비자(consumer)는 super를 사용한다.

ComparableComparator는 소비자라는 사실도 기억하자.

0개의 댓글