[Effective Java] 아이템 31 : 한정적 와일드카드를 사용해 API 유연성을 높이라

Loopy·2022년 8월 6일
0

이펙티브 자바

목록 보기
30/76
post-thumbnail

매개변수화 타입은, 불공변(invariant) 이다.

🔖 불공변
서로 다른 타입 Type1Type2 가 있을 때 List<Type1>List<Type2>의 하위 타입도 상위 타입도 아닌 것을 의미

List<String>은 문자열만, List<Object>는 어떤 객체도 넣을 수 있으니 후자가 하는 일을 제대로 수행하지 못해 당연히 하위 타입이 될 수 없기 때문에 어찌보면 당연하다. 하지만, 이러한 불공변 방식은 유연하지 않다는 단점이 존재한다.

한정적 와일드카드를 사용한다면, API의 유연성을 높일 수 있다. 스택을 예로 들어 설명해보자.

예시 : 스택(STACK)

📚 스택 Public API

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);
}

와일드카드 타입을 사용하지 않은 popAll 메서드 : 결함 존재

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

해당 메서드는 src 의 원소 타입이 스택의 원소 타입과 일치하면 잘 작동한다. 하지만 Stack<Number>로 선언 후 pushAll()Integer 타입(하위 타입)의 컬렉션을 전달하면 매개변수화 타입이 불공변이기 때문에 오류가 난다.

StackTest.java:7:error: Iterable<Integer> cannot be converted to Iterable<Number>

IntegerNumber 의 하위 타입이지만, Iterable<Integer>Stack<Number> 의 관계는 불공변의 특성으로 인해 하위 타입이 되지 않기 때문이다.

바로 이러한 상황에서, 유연성을 위해 자바는 한정적 와일드카드 타입이라는 특별한 매개변수화 타입을 지원하고 있다!

☁️ 한정적 와일드 카드 타입 적용

유연성을 극대화하려면, 원소의 생산자나 소비자용 입력 매개변수에 와일드 카드 타입을 사용하면 된다.

🔖 PECS 공식
1) 매개변수화 타입 T 가 생산자 : <? extends T>
2) 매개변수화 타입 T 가 소비자 : <? super T>

pushAll()의 인자는 Stack 이 사용할 E 인스턴스를 생산하므로 생산자이고, popAll()의 인자는 Stack 으로부터 E 인스턴스를 소비하므로 소비자라고 볼 수 있다.

생산자(producer) 매개변수

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

Iterable<? extends E> 는 "E의 하위 타입의 Iterable" 이라는 의미이다. Stack 과 클라이언트 모두 깔끔히 컴파일 되기 때문에 타입이 안전하다고 볼 수 있다.

다시 말하면 E 의 여러 하위 타입 중에 어떤 타입일지 확정할 수 없으니, 자식 타입으로 원소를 꺼내고자 한다면 컴파일 에러가 발생하게 되어 부모 타입으로만 꺼낼 수 있다.

소비자(consumer) 매개변수

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

입력 매개변수의 타입이 "E의 상위 타입의 Collection" 임을 나타낸다.

다시 말하면 E 의 여러 부모들 중에서 어떤 타입일지 확정할 수 없으니, 부모 타입으로 추가하고자 할 경우에는 컴파일 에러가 발생하고 자식 타입이라면 안전하게 컬렉션에 추가할 수 있게 된다.

☁️ 한정적 와일드 카드 타입 주의사항

1. 반환 타입에는 불가능

public static <E> Set<E> union(Set<E> s1, Set<E> s2)
public static <E> Set<E> union(Set<? extends E> s1, Set<? extends E> s2)

s1s2 모두 E 의 생산자이니 PECS 공식에 따라 위와 같이 선언해야 한다. 주의할 점은, 반환 타입에는 한정적 와일드 카드 타입을 사용하면 안 된다는 것이다. (반환 타입은 그대로 Set<E>)

2. 자바 7 이전 버전은 명시적 타입 인수 사용

위의 코드는 목표 타이핑을 지원하는 자바 8부터 제대로 컴파일 되며, 자바 7까지는 타입 추론 능력이 충분히 강력하지 못해 문맥에 맞는 반환 타입을 명시해야 했다.

컴파일러가 올바른 타입을 추론하지 못할 때면, 언제든 명시적 타입 인수를 사용해 타입을 알려주면 된다.

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

3. Comparable, Comparator

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

와일드카드는, Comparable(혹은 Comparator)직접 구현한 다른 타입을 확장한 타입을 지원하기 위해 필요하다. 예를 들어, List<ScheduledFuture<?>> 에서 ScheduledFuture는 직접 구현하지 않고 그의 부모 DelayedComparable 을 구현하고 있는 상황처럼 말이다.

ComparableComparator 는 언제나 소비자이므로, Comparable<? super E> 처럼 사용하면 된다.

4. 타입 매개변수 VS 와일드카드

public static <E> void swap(List<E> list, int i, int j); // 비한정적 타입 매개변수

public static void swap(List<?> list, int i, int j); // 비한정적 와일드카드

메서드 선언에 타입 매개변수가 한번만 나오면, 와일드 카드로 대체해야 하는 것이 규칙이다. 비한정적 타입 매개변수라면 비한정적 와일드 카드로, 한정적 타입 매개변수라면 한정적 와일드카드로 변환하면 된다.

하지만 두번째 swap 은 해당 함수를 구현했을때 문제가 생긴다.

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

List<?> 에는 null 외에는 어떤 값도 넣을 수 없기 때문인데, 이 때 와일드 카드 타입의 실제 타입을 알려주는 메서드를 private 도우미 메서드로(제네릭) 작성하여 활용할 수 있다.

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

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

📚 핵심 정리
조금 복잡하더라도 와일드카드 타입을 적용하면 API가 훨씬 유연해진다. 생산자는 extends를 소비자는 super를 사용해야 한다는 PECS 공식을 꼭 기억하자. 또한, Comparable과 Comparator는 모두 소비자라는 사실도 잊지 말자.

profile
개인용으로 공부하는 공간입니다. 잘못된 부분은 피드백 부탁드립니다!

0개의 댓글