매개변수화 타입은 불공변이다.
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는 모두 소비자이다.
'Java > Effective Java' 카테고리의 다른 글
[Effective Java] Item 33. 타입 안전 이종 컨테이너를 고려하라 (0) | 2023.05.16 |
---|---|
[Effective Java] Item 32. 제네릭과 가변인수를 함께 쓸 때는 신중하라 (0) | 2023.05.15 |
[Effective Java] Item 30. 이왕이면 제네릭 메서드로 만들라 (1) | 2023.05.09 |
[Effective Java] Item 29. 이왕이면 제네릭 타입으로 만들라 (0) | 2023.05.09 |
[Effective Java] Item 28. 배열보다는 리스트를 사용하라 (0) | 2023.05.09 |