NMP-Study / EffectiveJava2022

Effective Java Study 2022
5 stars 0 forks source link

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

Closed okhee closed 2 years ago

mbyul commented 2 years ago

매개변수화 타입은 불공변(invariant) #28

case1. Stack의 pushAll 메서드 구현


public class Stack<E> {
public Stack(){ ... };
public void push(E e){ ... };
public E pop() { ... };
public boolean isEmpty(){ ... };
// 와일드카드 타입을 사용하지 않은 pushAll 메서드 - 결함이 있다!
public void pushAll(Iterable<E> src) {
     for (E e : src) 
           push(e);

} }

- Iterable src의 원소 타입이 스택의 원소 타입과 일치하면 잘 동작
```JAVA
Stack<Integer> integerStack = new Stack<>();
Iterable<Integer> intVal = List.of(3, 5);
integerStack.pushAll(intVal);  // ok
Stack<Number> numberStack = new Stack<>();
Iterable<Integer> intVal = List.of(3, 5);
numberStack.pushAll(intVal);  // error

해결법 : 한정적 와일드카드 타입으로 변경

  • pushAll의 입력 매개변수 타입은 'E의 Iterable'이 아니라 'E의 하위 타입의 Iterable'이어야 한다.
  • Iterable<? extends E>
// ok
public void pushAll(Iterable<? extends E> src) {
   for (E e : src) 
      push(e);
}

case2. Stack의 popAll 메서드 구현

// 와일드카드 타입을 사용하지 않은 popAll 메서드 - 결함이 있다!
public void popAll(Collection<E> dst) {
while (!isEmpty())
dst.add(pop());
}
  • 매개변수 Collection dst의 원소 타입이 스택의 원소 타입과 일치하면 잘 동작
    Stack<Number> numberStack = new Stack<>();
    Collection<Number> numberVals = ...;
    numberStack.popAll(numberVals); // ok
  • 그럼 Stack<Integer>로 선언한 후 popAll(numberVals)을 호출하면 어떻게 될까?
  • numberVals은 Number 타입
    Stack<Integer> integerStack = new Stack<>();
    Collection<Number> numberVals = ...;
    integerStack.popAll(numberVals); // error
  • Collection <Number>Collection<Integer>의 하위 타입이 아니므로 오류 발생 !

해결법 : 한정적 와일드카드 타입으로 변경

  • popAll의 입력 매개변수의 타입이 'E의 Collection'이 아니라 'E의 상위 타입의 Collection'이어야 함
  • Collection<? super E>
// ok
public void popAll(Collection<? super E> dst) {
   while (!isEmpty())
       dst.add(pop());
}

공식 : 팩스(PECS) producer-extends, consumer-super

case3. #28 의 Chooser 생성자


class Chooser<T> {
private final List<T> choiceList;
public Chooser(Collection<T> choices) {
    this.choiceList = new ArrayList<>(choices);
}

... }

- 생성자에 넘겨지는 choices 컬렉션은 T 타입의 값을 생산하기만 함
- `Chooser<Number>`의 생성자에 `List<Integer>`를 매개변수로 넘긴다면?
   - **컴파일 에러**

> 해결법 : PECS 공식에 따라 매개변수 Collection 원소 타입이 생산자이므로 Producer-extends 규칙에 따라 변경
```JAVA
public Chooser(Collection<? extends T> choices){...} // ok

case4. #30-2 unoin 메서드


public class GenericMethodTest {
public static <E> Set<E> union(Set<E> s1, Set<E> s2) {
Set<E> result = new HashSet<>(s1);
result.addAll(s2);
return result;
}
public static void main(String[] args) {
    Set<Integer> integers = Set.of(1, 3, 5);
   Set<Double> doubles = Set.of(2.0, 4.0, 6.0);
   Set<Number> numbers = union(integers, doubles); // error
}

}

- 매개변수 s1은 `Set<Integer>`, s2는 `Set<Double>` / 반환타입은 `Set<Number>`
   - `Set<Integer>`와 `Set<Double>`은 `Set<Number>`의 상위 타입도 하위타입도 아님 (타입 불공변)
> 해결법 : s1과 s2 모두 E의 생산자이니 PECS 공식에 의하여 다음과 같이 수정해야함
```JAVA
public static <E> Set<E> union(Set<? extends E> s1, Set<? extends E> s2){ ... }

컴파일러가 올바른 타입을 추론하지 못한다면, 명시적 타입 인수 사용하여 해결 (자바5 / 6 / 7 버전)

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

여기서 주의 !

  • 반환 타입은 여전히 Set<E>
  • 반환 타입에는 한정적 와일드카드 타입을 사용하면 안된다.
  • 클라이언트 코드에서도 와일드카드 타입을 써야 함

case5. #30-7의 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 공식을 두번 적용
   - 입력 매개변수 : `List<E> list -> List<? extends E> `
        - E 인스턴스를 생산하므로 Producer-Extends 공식에 의해 수정되어짐
   - 타입 매개변수 : `<E extends Comparable> -> <E extends Comparable<? super E>>`
       - 처음 E가 `Comparable<E>`를 확장한다고 정의했는데, 이때` Comparable<E>`는 E 인스턴스를 소비한다
       - 그래서 Consumer-Super 공식에 의해 수정되어짐
      - Comparable은 언제나 소비자이므로, 일반적으로 `Comparable<E>`보다는 `Comparable<? super E>`를 사용 (Comparator도 마찬가지)

> 이렇게 복잡하게 만들 가치가 있는걸까? 
  - 다음 리스트는 오직 수정된 max로만 처리할 수 있음
```java
List<ScheduledFuture<?>> scheduledFutures = ...;

메서드 선언에 타입 매개변수가 한 번만 나오면 와일드카드로 대체하라

// 2. 비한정적 와일드카드 사용 public static void swap(List<?> list, int i, int j);

-public API라면 간단한 두번째가 나음
   - 어떤 리스트든 이 메서드에 넘기면 명시한 인덱스의 원소들을 교환 
   - 신경 써야 할 타입 매개변수도 없음

## 규칙 : 메서드 선언에 타입 매개변수가 한 번만 나오면 와일드카드로 대체하라 
  - 비한정적 타입 매개변수라면 비한정적 와일드카드로 바꾸고, 한정적 타입 매개변수라면 한정적 와일드카드로 바꿔라

> case1. 방금 꺼낸 원소를 리스트에 다시 넣는 코드
```java
public static void swap(List<?> list, int i, int j) {
    list.set(i, list.set(j, list.get(i))); // error
}

image

// 와일드카드 타입을 실제 타입으로 바꿔주는 private 도우미 메서드 private static void swapHelper(List list, int i, int j){ list.set(i, list.set(j, list.get(i))); }


- swapHelper 메서드는 리스트가 `List<E>`임을 알고 있음
   - 즉, 이 리스트에서 꺼낸 값은 항상 E이고, E타입의 값이라면 이 리스트에 넣어도 안전함을 알고 있음

# 정리
- 조금 복잡하더라도 와일드카드 타입을 적용하면 API가 훨씬 유연해진다!
- 널리 쓰일 라이브러리를 작성한다면 반드시 와일드카드 타입을 적절히 사용하자!
- PECS 공식을 기억하자!
- Comparable과 Comparator는 모두 소비자다!