아이템 31. 한정적 와일드카드를 사용해 API 유연성을 높이라

문법식·2022년 8월 16일
0

Effective Java 3/E

목록 보기
31/52

아이템 28에서 이야기했듯 매개변수화 타입은 불공변이다. 즉, 서로 다른 타입 Type1Type2가 있을 때 List<Type1>List<Type2>의 하위 타입도 상위 타입도 아니다.
하지만 때론 불공변 방식보다 유연한 방식이 필요하다. 아이템 29의 Stack 클래스를 예로 설명하겠다.

Stackpublic API

public class Stack<E>{
	public Stack();
    public void push(E e);
    public E pop();
    public boolean isEmpty();
}

여기에 일련의 원소를 넣는 메서드를 추가한다.

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

이 메서드는 컴파일 잘 되지만 완벽하지 않다. src의 원소 타입이 스택의 원소 타입과 일치하면 잘 작동한다. 하지만 Stack<Number>로 선언한 후 Integer 타입인 intValpushAll(intVal)을 호출하면 오류 메시지가 발생한다. 매개변수화 타입이 불공변이기 때문에 IntegerNumber의 하위 타입이라도 오류가 발생한 것이다.

자바는 이런 상황에서 한정적 와일드카드 타입이라는 특별한 매개변수화 타입을 지원한다. pushAll의 입력 매개변수 타입은 'EIterable'이 아니라 'E의 하위 타입의 Iterable'이어야 하며, 와일드카드 타입 Iterable<? extends E>가 정확히 이런 뜻이다.

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

위와 같이 수정하면 타입 안전하게 컴파일 잘 된다. 이제 popAll 메서드를 작성해본다.

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

이번 메서드도 컴파일 잘 되지만 완벽하지 않다. Stack<Number>의 원소를 Object용 컬렉션으로 옮기려하면 오류가 발생한다. 아까와 마찬가지로 매개변수화 타입은 불공변이기 때문이다. 이번에도 와일드카드 타입으로 해결할 수 있다. popAll의 입력 매개변수의 타입이 'ECollection'이 아니라 'E의 상위 타입의 Collection'이어야 한다. Collection<? super E>가 정확이 이런 뜻이다.

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

위와 같이 수정하면 타입 안전하게 컴파일 잘 된다.

와일드카드 타입이 주는 메시지는 분명하다. 유연성을 극대화하려면 원소의 생산자나 소비자용 입력 매개변수에 와일드카드 타입을 사용하라. 한편, 입력 매개변수가 생산자와 소비자 역할을 동시에 한다면 와일드카드 타입을 써도 좋을 게 없다. 타입을 정확히 지정해야 하는 상황으로, 이때는 와일드카드 타입을 쓰지 말아야 한다.
다음 공식을 외워두면 어떤 와일드카드 타입을 써야하는지 기억하는 데 도움이 된다.

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

즉, 매개변수화 타입 T가 생상자라면 <? extend T>를 사용하고, 소비자라면 <? super T>를 사용하라는 것이다. Stack의 예에서 pushAllsrc 매개변수는 Stack이 사용할 E 인스턴스를 생산하므로 srcIterable<? extends E> 타입으로 정의되었다. 한편, popAlldst 매개변수는 Stack으로부터 E 인스턴스를 소비하므로 dst의 적절한 타입은 Collection<? super E>이다.

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

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

앞의 코드는 자바 8부터 제대로 컴파일 된다. 자바 7까지는 타입 추론 능력이 충분히 강력하지 못해서 문맥에 맞는 반환 타입을 명시해야 했다. 컴파일러가 올바른 타입을 추론하지 못할 때면 언제든 아래와 같이 명시적 타입 인수를 사용해서 타입을 알려주면 된다.

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

max 메서드를 와일드카드 타입을 사용한 코드로 변경했다.

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

Comparable은 언제나 소비자이므로, 일반적으로 Comparable<E>보다는 Comparable<? super E>를 사용하는 편이 낫다. Comparator도 마찬가지이다. 그러면 Comparable을 직접 구현하지 않고, 직접 구현한 다른 타입을 확장한 타입을 지원해준다.

메서드를 정의할 때 타입 매개변수와 와일드카드 중 어떤 선언이 더 나을까? public API라면 와일드카드가 낫다. 기본 규칙은 메서드 선언에 타입 매개변수가 한 번만 나오면 와일드카드로 대체해라. 라는 것이다. List<?>에는 null 외에는 어떤 캆도 넣을 수 없다. 만약에 값을 넣고 싶으면 와일드카드 타입의 실제 타입을 알려주는 메서드를 private 도우미 메서드로 작성하여 해결하면 된다.

profile
백엔드

0개의 댓글