-
31. 한정적 와일드카드를 사용해 API 유연성을 높이라.JAVA/Effective java 2021. 3. 16. 16:02
- 제네릭에서 매개변수화 타입은 불공변이라 하였다. 즉 서로 다른 타입 Type1과 Type2가 있을 때 List<Type1> 은 List<Type2> 의 하위 타입도 상위 타입도 아니다.
- List<String>은 List<Object>의 하위 타입이 아니라는 뜻인데, 즉 List<Object>에는 어떤 객체든 넣을 수 있지만 List<String> 에는 문자열만 넣을 수 있다.
- 즉 List<String> 은 List<Object>가 하는 일을 제대로 수행하지 못하니 하위 타입이 될 수 없다.
- 하지만 때론 불공변 방식보다 유연한 무언가가 필요하다.
- 여기에 일련의 원소를 스택에 넣는 메서드를 추가해야 한다고 해보자.
- 이 메서드는 깨끗이 컴파일되지만 완벽하진 않다.
- Iterable src의 원소타입이 스택의 원소 타입과 일치하며 잘 작동한다.
- 하지만 Stack<Number>로 선언한 후 pushAll(intVal) 을 호출하면 어떻게 될까? intVal은 Integer 타입이다.
- Integer는 Number의 하위 타입이니 잘 작동한다. 논리적으로는 잘 작동해야 할 것 같다.
- 실제로는 다음의 오류 메세지가 뜬다. 매개변수화 타입이 불공변이기 때문이다.
- 자바는 이런 상황에 대처할 수 있는 한정적 와일드카드 타입이라는 특별한 매개변수화 타입을 지원한다.
- pushAll의 입력 매개변수 타입은 'E의 Iterable' 이 아니라 'E의 하위 타입의 Iterable' 이어야 하며, 와일드 카드 타입 Iterable<? extends E>가 정확히 이런 뜻이다.
- 이번 수정으로 Stack은 물론 이를 사용하는 클라이언트 코드도 말끔히 컴파일 된다.
- Stack과 클라이언트 모두 깔끔히 컴파일되었다는 건 모든 것이 타입 안전하다는 뜻이다.
- 이제 pushAll과 짝을 이루는 popAll 메서드를 작성할 차례다.
- popAll 메서드는 Stack 안의 모든 원소를 주어진 컬렉션으로 옮겨 담는다.
- 이번에도 주어지니 컬렉션의 원소 타입이 스택의 원소 타입과 일치한다면 말끔히 컴파일되고 문제없이 동작한다.
- 하지만 역시 완벽하지 않다. Stack<Number>의 원소를 Object용 컬렉션으로 옮기려 한다고 해보자.
- 이번에도 비슷한 오류가 발생한다.
- popAll 코드와 함께 컴파일하려 한다면 "Collection<Object>는 Collection<Number>의 하위 타입이 아니다" 라는 오류와 비슷한 오류가 발생한다.
- 이번에도 와일드카드 타입으로 해결할 수 있다.
- 이번에는 popAll의 입력 매개변수의 타입이 'E의 Collection'이 아니라 'E의 상위 타입의 Collection'이어야 한다.
- 와일드카드 타입을 사용한 Collection<? super E> 가 정확히 이런 의미이며 이를 popAll에 적용시켰다.
- 메세지는 분명하다. 유연성을 극대화하려면 원소의 생산자나 소비자용 입력 매개변수에 와일드카드 타입을 사용해야 한다.
- 한편 입력 매개변수가 생산자와 소비자 역할을 동시에 한다면 와일드카드 타입을 써도 좋을 게 없다.
producer-extends, consumer-super - 매개변수화 타입 T가 생산자라면 <? extends T> 를 사용하고, 소비자라면 <? super T>를 사용하라.
- Stack 예에서 pushAll의 src 매개변수는 Stack이 사용할 E 인스턴스를 생산하므로 src의 적절한 타입은 Iterable<? extends E> 이다.
- 한편, popAll의 dst 매개변수는 Stack으로부터 E 인스턴스를 소비하므로 dst의 적절한 타입은 Collection<? super E>이다.
- 아이템 28의 Chooser 생성자는 T 타입의 값을 생사하기만 하니 와일드 카드 타입을 통해 개선해 보았다.
- 개선 이후 Chooser<Number>의 생성자에 List<Integer>을 넘기는 것이 가능해지는 것을 알 수 있다.
- 아이템30 역시 s1과 s2 모두 E의 생산자이니 다음처럼 선언하도록 바꾸어주었다.
- 제대로만 사용한다면 클래스 사용자는 와일드카드 타입이 쓰였다는 사실조차 의식하지 못한다.
- 받아들여야 할 매개변수를 받고 거절해야 할 매개변수는 거절하는 작업이 알아서 이뤄진다.
- 클래스 사용자가 와일드카드 타입을 신경 써야 한다면 그 API에 무슨 문제가 있을 가능성이 크다.
- 아이템 30의 max 메서드 역시 개선해보았다.
- 와일드카드 타입을 두번 적용했는데 둘 중 더 쉬운 입력 매개변수 목록부터 살펴보자.
- 입력 매개변수에서는 E 인스턴스를 생산하므로 원래의 List<E>를 List<? extends E>로 수정했다.
- 다음은 타입 매개변수 E인데 원래 선언에서는 E가 Comparable<E>를 확장한다고 정의했다.
- 이 때 Comparable<E>는 E 인스턴스를 소비한다. 그래서 매개변수화 타입 Comprable<E>를 한정적 와일드카드 타입인 Comparable<? super E>로 대체했다.
- Comparable은 언제나 소비자이므로, 일반적으로 Comparable<E> 보다는 Comparable<? super E>를 사용하는 편이 낫다. Comparator도 마찬가지다.
이렇게 복잡하게 만들 가치가 있을까?
- 답은 '그렇다'이다.
- 그 근거로, 다음 리스트는 오직 수정된 max로만 처리할 수 있다.
- 수정 전 max는 ScheduledFuture가 Comparablee<ScheduledFuture>을 구현하지 않았기 때문에 처리할 수 없었다.
- ScheduledFuture는 Delayed의 하위 인터페이스이고, Delayed는 Comparable<Delayed>를 확장했다.
- 다시 말해, ScheduledFuture의 인스턴스는 다른 ScheduledFuture 인스턴스뿐아니라 Delayed 인스턴스와도 비교할 수 있어서 수정 전 max가 거부하는 것이다.
- 어떤 선언이 더 나을까? public API 라면 간단한 두번째가 더 낫다.
- 어떤 리스트든 이 메서드에 넘기면 명시한 인덱스의 원소들을 교환해줄 것이다. 신경 써야 할 타입 매개변수도 없다.
- 기본 규칙은 이렇다. 메서드 선언에 타입 매개변수가 한 번만 나오면 와일드카드로 대체하자.
- 이때 비한정적 타입 매개변수라면 비한정적 와일드카드로 바꾸고, 한정적 타입 매개변수라면 한정적 와일드카드로 바꾸면 된다.
- 하지만 두 번째 swap 선언에는 문제가 하나 있는데 다음과 같은 직관적으로 구현한 코드가 컴파일되지 않는다는 것이다.
- 원인은 리스트의 타입이 List<?> 인데, List<?> 에는 null외에는 어떤 값도 넣을 수 없다는 데 있다.
- 이때 런타임 오류를 낼 수 있는 형변환, 리스트의 로 타입을 사용하지 않고 와일드카드 타입의 실제 타입을 알려주는 private 도우미 메서드를 따로 작성하여 활용하는 방법이 있다.
- swapHelper 메서드는 리스트가 List<E> 임을 알고 있다.
- 즉 이 리스트에서 꺼낸 값의 타입은 항상 E이고, 이 리스트에 넣어도 안전함을 알고 있다.
- swap 메서드를 호출하는 클라이언트는 복잡한 swapHelper의 존재를 모른 채 그 혜택을 누리게 된다.
핵심 정리
- 조금 복잡하더라도 와일드카드 타입을 적용하면 API가 훨씬 유연해진다.
- 널리 쓰일 라이브러리를 작성한다면 반드시 와일드카드 타입을 적절히 사용해줘야 한다.
- 생산자는 extends를 소비자는 super을 사용하며 Comparable과 Comparator는 모두 소비자라는 사실도 잊지말자.
참고 자료
'JAVA > Effective java' 카테고리의 다른 글
아이템 33. 타입 안전 이종 컨테이너를 고려하라 (0) 2021.03.22 아이템 32. 제네릭과 가변인수를 함께 쓸 때는 신중하라. (0) 2021.03.20 30. 이왕이면 제네릭타입으로 만들라2 (0) 2021.03.15 29. 이왕이면 제네릭 타입으로 만들라 (0) 2021.03.14 28. 배열보다는 리스트를 사용하라 (0) 2021.03.13