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

gang_shik·2022년 5월 21일
0

Effective Java 5장

목록 보기
6/6
  • 매개변수화 타입은 불공변임 서로 다른 타입 Type1Type2 가 있을 때 List<Type1>List<Type2>하위 타입도 상위 타입도 아님

  • List<String>List<Object>의 하위 타입이 아니라는 뜻임

  • 따지고 보면 List<Object> 는 어떤 객체든 넣을 수 있지만 List<String> 에는 문자열만 넣을 수 있음 List<String>List<Object> 가 하는 일을 제대로 수행하지 못해 하위 타입이 될 수 없음

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

  • 예를 들어 일련의 원소를 스택에 넣는 메서드가 있을 때

public void pushAll(Iterable<E> src) {
		for (E e : src)
				push(e);
}
  • 위와 같이 쓴다면 컴파일이 되도 완벽하진 않음, Iterable src 의 원소 타입이 스택의 원소 타입과 일치하면 잘 작동하지만 타입이 다르면 오류 메시지가 뜸

  • Number 타입 대신 Integer 가 들어가도 오류가 뜸 하위타입이어도 그 이유는 매개변수화 타입이 불공변이기 때문임

  • 이런 상황에 대처하기 위해서 한정적 와일드카드 타입이라는 특별한 매개변수화 타입을 지원함

public void pushAll(Iterable<? extends E> src) {
		for (E e : src)
				push(e);
}
  • 위와 같이 쓴다면 매개변수 타입은 E의 Iterable 이 아니라 E의 하위 타입의 Iterable 이어야 하며, 와일드카드 타입 Iterable<? extends E> 가 정확히 이런 뜻임(이 extends 가 확장한 것을 의미한 것은 아님)

  • 위처럼 적용하게 된다면 컴파일이 말끔히 됨

  • 여기서 popAll 메서드 역시 Collection<Object>Collection<Number> 가 불공변이므로 하위 타입이 아니기 때문에 이 역시 와일드카드 타입을 적용해야함

public void popAll(Collection<? super E> dst) {
		while (!isEmpty())
				dst.add(pop());
} 
  • 위와 같이 쓰면 입력 매개변수의 타입이 E의 Collection 이 아니라 E의 상위 타입의 Collection이어야 한다는 의미가 됨

  • 여기서 전달하는 메시지는 분명함, 유연성을 극대화하려면 원소의 생산자나 소비자용 입력 매개변수에 와일드카드 타입을 사용하는 것임

  • 하지만 입력 매개변수가 생산자와 소비자 역할을 동시에 한다면 와일드카드 타입을 써도 좋을게 없음, 타입을 정확히 지정해야 하는 상황이므로

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

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

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

  • 앞서 아이템에서 본 메서드와 생성자 선언 역시 수정할 수 있음

public Chooser(Collection<T> choices)
  • 위 생성자로 넘겨지는 choices 컬렉션은 T 타입의 값을 생산하기만 하니 T 를 확장하는 와일드카드 타입을 사용해 선언해야함
public Chooser(Collection<? extends T> choices)
  • union 메서드 역시 수정 가능함 모두 E 의 생성자이므로 아래와 같이 씀
public static <E> set<E> union(Set<? extends E> s1, Set<? extends E> s2)
  • 이런식으로 제대로만 사용하면 클래스 사용자는 와일드카드 타입이 쓰였다는 사실조차 의식하지 못함, 받아들여야 할 매개변수를 받고 거절해야 할 매개변수는 거절하는 작업이 알아서 이뤄짐

  • 하지만 위의 코드는 자바 8부터 제대로 컴파일 됨 자바 7까지는 타입 추론 능력이 충분히 강력하지 못해서 문맥에 맞는 반환 타입(혹은 목표 타입)을 명시해야함

  • 아래와 같이 자바 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 공식을 두 번 적용함, 입력 매개변수에서는 E 인스턴스를 생산하므로 List<? extends E> 로 수정함

  • 그 다음 타입 매개변수를 보면 원래 선언에선 EComparable<E> 를 확장한다고 정의했는데 이때 Comparable<E>E 인스턴스를 소비함, 그래서 한정적 와일드카드 타입인 Comparable<? super E> 로 대체함

  • Comparable 은 언제나 소비자이므로, 일반적으로 Comparable<E> 보다는 Comparable<? super E> 를 사용하는 편이 나음

  • Comparator 역시 마찬가지임

  • 이렇게 하는 이유는 위와 같이 수정된 메서드에서만 List<ScheduledFuture<?>> scheduledFutures = ...; 를 쓸 수 있음

  • 이는 Comparable을 직접 구현하지 않고 직접 구현한 다른 타입을 확장한 타입을 지원하기 위해 와일드카드가 필요하기 때문임

  • 무슨말이냐면 ScheduledFuture 자체가 Delayed 의 하위 인터페이스이고 DelayedComparable<Delayed> 를 확장했기 때문에 수정전 max 는 불공변이므로 이를 타입이나 하위 관계로써 생각하지 않기 때문에 와일드카드를 써야하는 것임

  • 하나 더 논의할 주제가 있는데 타입 매개변수와 와일드카드에는 공통된 부분이 있어서, 메서드를 정의할 때 둘 중 어느 것을 사용해도 괜찮을 때가 많음

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<?> 인데 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)));
}
  • swapHelper 메서드는 리스트가 List<E> 임을 알고 있음, 이 리스트에서 꺼낸 값의 타입은 항상 E 이고, E 타입의 값이라면 이 리스트에 넣어도 안전함을 알고 있음

  • swap 메서드 내부에서는 더 복잡한 제네릭 메서드를 이용했지만, 덕분에 외부에서는 와일드카드 기반의 멋진 선언을 유지할 수 있었음

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

profile
측정할 수 없으면 관리할 수 없고, 관리할 수 없으면 개선시킬 수도 없다

0개의 댓글