본문 바로가기
Java/Effective Java 3E

[이펙티브자바 3판] ITEM31. 한정적 와일드카드를 사용해 API 유연성을 높이라

by 잭피 2020. 10. 18.

이번장의 핵심은...

조금 복잡하더라도 와일드카드 타입을 적용하면 API가 훨씬 유연해진다

그러니 널리 쓰일 라이브러리를 작성한다면 반드시 와일드카드 타입을 적절히 사용해줘야 한다

PECS 공식을 기억하자

즉, 생산자(producer)는 extends를 소비자(consumer)는 super를 사용한다

Comparable과 Comparator는 모두 소비자라는 사실도 잊지 말자


매개변수화 타입은 불공변

즉, 서로 다른 타입 Type1, Type2가 있을 때, List<Type1>은 List<Type2>의 상위 타입도 하위 타입도 아니다

때론 불공변 방식보다 유연한 무언가가 필요하다

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

Iterable src의 원소 타입이 스택의 원소 타입과 일치하면 문제없다

하지만, 만약 Stack<Number>로 선언한 후, pushAll(intVal)을 호출한다면? (intVal은 Integer 타입)

→ Integer는 Number의 하위 타입이니 잘 동작할 것 같지만, 오류가 발생

매개변수화 타입이 불공변이기 때문이다

한정적 와일드카드 타입

다행히 위의 해결책으로 한정적 와일드카드 타입이라는 특별한 매개변수화 타입을 지원

pushAll의 입력 매개변수 타입은 E의 Iterable이 아닌 E의 하위 타입의 Iterable 이어야 하며,

와일드카드 타입 Iterable<? extends E>가 정확히 이런 뜻이다

와일드카드 타입을 사용하도록 pushAll을 수정

// E 생산자(producer) 매개변수에 와일드카드 타입 적용
public void pushAll(Iterable<? extends E> src) {
	for (E e : src) push(e);
} 

→ 이제 모두 깔끔히 컴파일 o (타입이 안전)

// 와일드카드 타입을 사용하지 않은 popAll 메서드 - 결함이 있다!
public void popAll(Collection<E> dst) {
	while (!isEmpty()) dst.add(pop());
}
// 위 코드 역시 원소 타입이 일치하면 문제가 없지만 일치하지 않으면 오류가 발생
// E 소비자 매개변수에 와일드카드 타입 적용
public void popAll(Collection<? super E> dst) {
	while (!isEmpty()) dst.add(pop());
}

 

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

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

즉, 매개변수화 타입 T가 생산자라면 <? extends T>를 사용하고, 소비자라면 <? super T>를 사용하라

PECS 공식은 와일드카드 타입을 사용하는 기본 원칙이다

이 공식을 기억하고 앞 장 예제를 다시 공식에 맞게 수정

// 생산자 매개변수에 와일드카드 타입 적용
public Chooser(Collection<? extends T> choices)

Chooser<Number> 생성자에 List<Integer>를 넘기고 싶을 때,

기존에는 컴파일조차 안 되겠지만 이렇게 한정적 와일드카드 타입으로 수정 후 문제가 사라짐

public static <E> Set<E> union(Set<E> s1, Set<E> s2)
// s1, s2 모두 E의 생산자이니 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);

반환 타입은 여전히 Set<E> 반환 타입에는 한정적 와일드카드 타입을 사용하면 안 된다 유연성을 높여주기는커녕 클라이언트 코드에서도 와일드카드 타입을 써야 하기 때문이다

 

제대로 사용한다면 클래스 사용자는 와일드카드 타입이 쓰였다는 사실조차 의식하지 못할 것

클래스 사용자가 와일드카드 타입을 신경 써야 한다면 그 API에 무슨 문제가 있을 가능성이 크다

 

앞의 코드는 자바 8부터 제대로 컴파일, 자바 7에선 문맥에 맞는 반환 타입을 명시해야 한다

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

이번엔 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 공식을 2번 적용했다

먼저, 입력 매개변수에서는 E 인스턴스를 생산하므로 원래의 List<E>를 List<? extends E>로 수정

다음은 타입 매개변수 E이다

원래 선언에서는 E가 Comparable<E>를 확장한다고 정의했는데,

이때 Comparable<E>는 E 인스턴스를 소비한다 (그리고 선후 관계를 뜻하는 정수를 생산한다)

 

Comparable은 언제나 소비자이므로, 일반적으로 Comparable<E>보다는 Comparable<? super E>를 사용하는 편이 낫다 Comparator도 마찬가지로, 일반적으로 Comparator<E> 보다는 Comparator<? super E>를 사용하는 편이 낫다

 

와일드카드

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

예) 주어진 리스트에서 명시한 2개의 인덱스의 아이템들을 교환하는 정적 메서드를 두 방식으로 정의

 

1. 비한정적 타입 매개변수 사용

2. 비한정적 와일드카드 사용

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

어떤 선언이 나을까?

public API라면 간단한 2번째가 낫다

 

* 기본 규칙

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

이때 비한정적 타입 매개변수라면 비한정적 와일드카드로 바꾸고, 한정적 타입 매개변수이면 한정적 와일드카드로 바꾸자

 

하지만 2번째 swap에는 문제가 하나 있음 (컴파일이 안됨)

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> list, int i, int j) {
	list.set(i, list.set(j, list.get(i)));
}

swap 메서드를 호출하는 클라이언트는 복잡한 swapHelper의 존재를 모른 채 그 혜택을 누림

 

 

 

이 글은 “이펙티브 자바 3판” 책 내용을 정리한 글입니다.

만약 저작권 관련 문제가 있다면 “shk3029@kakao.com”로 메일을 보내주시면, 바로 삭제하도록 하겠습니다.

 

댓글