맞는데 왜 틀릴까..?

Java/Effective Java

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

안도일 2023. 5. 15. 22:40

매개변수화 타입은 불공변이다. 

List<String>은 List<Object>의 하위 타입이 아니다.

이러한 문제로 인해 발생하는 오류를 해결하기 위해 한정적 와일드카드를 사용한다.

 

한정적 와일드카드

 

매개변수화 타입 T

생산자 :  <? extends T>

소비자 :  <? super T>

 

Item29의 Stack 클래스를 예로 들어보자.

 

와일드카드 타입을 사용하지 않은 pushAll 메서드

 

 

  • Iterable src의 원소 타입이 Stack 원소 타입과 일치해야 작동한다.
  • Stack<Number>로 선언한 후 Iterable<Integer>를 src로 넘겨 호출하면 동작하지 않는다.

Integer는 Number의 하위 타입이니 잘 동작하리라 생각하지만 매개변수화 타입은 불공변이기 때문에 오류가 발생한다.

 

Stack<Number>를 선언하면 Stack에 Integer가 아닌 Number의 하위 타입도 포함될 수 있다. 따라서 Stack<Number>에 Iterator<Integer>를 넘겨준다면, Iterator<Integer>는 Integer 타입만 반환할 수 있기 때문에 Stack에 저장될 수 있는 Number의 다른 하위 타입들과 호환되지 않는다.

 

 

매개변수에 와일드카드 타입을 적용한 pushAll 메서드

 

 

  • Iterable<? extends E> : E 자기 자신 또는 E의 하위 타입의 Iterable
  • 불공변으로 인해 타입이 다르면 오류가 발생하지만 매개변수에 와일드카드 타입을 적용하여 하위 타입까지 호환된다.

 

pushAll의 src 매개변수는 Stack이 사용할 E 인스턴스를 생산하므로 Iterable<? extends E>를 사용하자.

 

 

와일드카드 타입을 사용하지 않은 popAll 메서드

 

 

  • ICollection의 원소 타입이 Stack 원소 타입과 일치해야 작동한다.
  • Stack<Number>로 선언한 후 Collection<Object>를 dst로 넘겨 호출하면 동작하지 않는다

Stack<Number>의 원소를 Object용 컬렉션으로 옮기려고 하지만 Collection<Object>는 Collection<Number>의 하위 타입이 아니기 때문이다.

 

 

매개변수에 와일드카드 타입을 적용한 popAll 메서드

 

 

  • Collection<? super E> : E 또는 E의 상위 타입을 가지는 Collection
  • 불공변으로 인해 타입이 다르면 오류가 발생하지만 매개변수에 와일드카드 타입을 적용하여 상위 타입까지 호환된다.

 

popAll의 dst 매개변수는 Stack으로부터 E 인스턴스를 소비하므로 Collection<? super E>를 사용하자.

 


여러 가지 예시

 

 

Item28 Chooser 생성자

 

 

  • 생성자로 넘겨지는 choices 컬렉션은 T 타입의 값을 생산하기만 하기 때문에 T를 확장하는 와일드카드 타입을 사용해 선언하자.

 

 

Chooser<Number>의 생성자에 List<Integer>를 넘긴다고 했을 때 수정 전 생성자는 컴파일 조차 되지 않는다.

 

 

 

union 메서드

 

 

  • s1과 s2 모두 E의 생산자이므로 E를 확장하는 와일드카드 타입을 사용해 선언하자.

 

 

  • 클라이언트 코드에서도 와일드카드 타입을 사용해야 하기 때문에 반환 타입에는 한정적 와일드카드 타입을 사용하면 안 된다. 

 

 

코드를 수정하면 위와 같은 Integer와 Double을 union하는 코드도 잘 실행된다.

 

 

 

max 메서드

 

 

  • 입력 매개변수 : E 인스턴스를 생산하므로 원래의 List<E>를 List<? extends E>로 수정하자.
  • 타입 매개변수 : 원래 선언에서 E가 Comparable<E>를 확장한다고 정의했는데, 이때 Comparable<E>는 E인스턴스를 소비한다. 따라서 <E extends Comparable<? super E>>로 수정하자.

 

 

  • 일반적으로 Comparable과 Comparator은 Comparable<E> 보다 Comparable<? super E>를 사용하는 편이 낫다.
  • Comparable 혹은 Comparator를 직접 구현하지 않고, 직접 구현한 다른 타입을 확장한 타입을 지원하기 위해 max 메서드처럼 와일드카드가 필요하다.

 

 

 


타입 매개변수 vs 와일드카드

 

비한정적 매개변수 (List<E>) : 어떤 타입이든 받을 수 있다.

한정적 매개변수 (List<E extends Number>) : Number 클래스나 그 하위 클래스의 인스턴스만 받을 수 있다.

비한정적 와일드카드 (List<?>) : 어떤 타입이든 받을 수 있다.

한정적 와일드카드 (List<? extends Number>) : Number 클래스나 그 하위 클래스의 인스턴스만 받을 수 있다.

 

매개변수는 제네릭 타입을 정의하는 데 사용되고, 와일드카드는 제네릭 타입을 불특정하게 표현하거나 제한을 설정하는 데 사용된다.

 

 

타입 매개변수와 와일드카드에는 공통되는 부분이 있어 아래와 같이 swap 메서드를 구현하는 두 가지 방법이 있다.

어떤 선언이 더 나을까?

 

비한정적 타입 매개변수 

 

비한정적 와일드카드

 

public API라면 신경 써야할 타입 매개변수가 없는 간단한 2번째 방식이 더 좋다.

 

비한정적 타입 매개변수 메서드를 비한정적 와일드카드 메서드로 만드는 방법은 간단하다. 

 

메서드 선언에 타입 매개변수가 한 번만 나오면 아래와 같은 방식으로 와일드카드로 대체하면 된다.

비한정적 타입 매개변수 -> 비한정적 와일드카드

한정적 타입 매개변수 -> 한정적 와일드카드

 

 

하지만 여기서 오류가 발생한다.

 

 

리스트의 타입이 List<?> 이므로 null 외에는 어떤 값도 넣을 수 없다. 따라서 와일드카드 타입의 실제 타입을 알려주는 private 도우미 메서드를 따로 작성하자.

 

 

swapHelper 메서드는 리스트가 List<E> 임을 알고 있기 때문에 리스트에서 꺼낸 값의 타입이 항상 E이고, E 타입의 값이라면 리스트에 넣어도 안전함을 알고 있다.

 

 

 

완성된 swap 메서드

 

 

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

 


 

요약

 

  • 조금 복잡하더라도 와일드카드 타입을 적용한 API가 훨씬 유연하다.
  • 생산자는 extends를, 소비자는 super를 사용하자.
  • Comparable과 Comparator는 모두 소비자이다.