NMP-Study / EffectiveJava2018

Effective Java Study
9 stars 0 forks source link

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

Closed madplay closed 5 years ago

seryang commented 5 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는 모두 소비자다!
seryang commented 5 years ago

Question

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

public static void swap(List<?> list, int i, int j) {
list.set(i, list.set(j, list.get(i))); // error
}
  • 방금 꺼낸 원소를 리스트에 다시 넣을 수 없어서 에러가 발생하는데 그 이유는 `List<?>에는 null 외에는 어떤 값도 넣을 수 없기 때문
  • 그럼 해결책으로 와일드카드 타입의 실제 타입을 알려주는 메서드 구현하라 하였고, 다음과 같은 도우미 메서드를 구현하면 된다고 책에서는 소개함
public static void swap(List<?> list, int i, int j){
   swapHelper(list, i, j);
}

private static <E> void swapHelper(List<E> list, int i, int j){
  list.set(i, list.set(j, list.get(i)));
}

Answer

그런식으로 해도 문제없습니다. 실제로 와일드카드를 쓰는 대다수의 경우는 타입파라미터를 사용하는쪽으로도 해결이 가능합니다. 그럼 이 두개가 뭔 차이인지를 알아야하는데요.

타입파라미터는 타입을 캡쳐하고 와일드카드는 하지않습니다. 즉 메서드 내부에서 타입을 알아야하는경우는 타입파라미터를 쓰고, 타입을 몰라도 되는경우는 와일드카드를 쓰는걸 권합니다.

가령 List를 받아서 그 size를 리턴하는경우엔 List가 어떤 요소를 담고있든 중요하지않죠. 이럴땐 타입파라미터가 아니라 와일드카드를 권합니다.

그런 관점에서 봤을때 swap은 내부 요소가 어떤건지 알필요가 없습니다. 타입이 어떤거든 상관없이 요소들의 순서를 변경해주는거니까요. 그런데 개념적으로는 와일드카드가 맞는데 실제로 와일드카드로 구현하면 기술적 한계때문에 메서드 구현이 안됩니다. 개념과 기술의 충돌이 발생하는거죠. 이때 그냥 속편하게 기술쪽을 택하면 글쓴분이 말씀하시는것처럼 구현하게되는거고요. 개념을 지키면서 기술의 한계를 넘을때 저렇게 헬퍼 메서드를 만들어서 해결하는겁니다.

책에서 말하고자하는 의도는 '개념적 접근' 입니다. 실제로 Collection 을 사용하시다보면 메서드가 어떤건 타입파라미터로 되어있고 어떤건 와일드카드로 되어있는데 이때 와일드카드로 되어있는건 '아 얘네는 내부타입이 뭐든 신경쓰지않겠구나' 라고 이해하시면 됩니다.

참고 - https://okky.kr/article/549764?note=1624089