peaches-book-study / effective-java

이펙티브 자바 3/E
0 stars 2 forks source link

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

Open youngkimi opened 3 months ago

youngkimi commented 3 months ago

Chapter : 5. 제네릭

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

Assignee : youngkimi


🍑 서론

오늘 배울 것

<? extends E>, <? super E>의 의미에 대해 알아보자.

매개변수화 타입은 불공변

서로 다른 타입 Type1, Type2가 있을 때, List 은 List의 상위 타입도, 하위 타입도 아니다. 마찬가지로 List가 List의 상위 타입이 아니다. List에는 문자열만, List 에는 모든 객체가 들어갈 수 있어 일을 대신 할 수 없다. (리스코프 치환 원칙 위배 : item 10)

그래도 때로는 유연함이 필요하다.

item 29의 Stack 클래스를 떠올려보자.

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 원소 타입이 스택의 원소 타입과 일치하면 잘 작동한다. 하지만 Stack로 선언 후 Number의 하위 타입인 IntegerpushAll()의 매개변수로 전달하면 어떻게 될까?

Integer는 Number의 하위 타입이니 논리적으로 잘 동작해야할 것 같다. 하지만 매개변수화 타입이 불공변이므로 오류 메세지가 뜬다.

자바는 이런 상황에 대처할 수 있는 한정적 와일드카드 타입이라는 특별한 매개변수화 타입을 지원한다.

🍑 본론

한정적 와일드카드 타입

요구사항 : pushAll()의 입력 매개변수 타입은 E의 Iterable이 아니라 E의 하위 타입의 Iterable 이다. 와일드 카드 타입 Iterable<? extends E>가 바로 그런 의미이다.

그럼 위에서 작성한 pushAll()한정적 와일드카드 타입을 사용해 수정해보자.

public void pushAll (Iterable<? extends E> src) {
    for (E e : src)
        push(e); 
}

이렇게 수정하면 Stack, Client 코드 모두에서 말끔히 컴파일된다. (타입이 안전하다.)

이번에는 pushAll()과 반대되는 popAll()을 작성해보자. popAll()은 Stack 내의 모든 원소를 주어진 컬렉션으로 옮겨 담는다.

public void popAll (Collection<E> dst) {
    while (! isEmpty())
        dst.add(pop())
}

위에서 발생했던 문제처럼, Stack<Number> 의 원소를 List<Object>으로 옮겨담을 때 문제가 발생할 것이다. 이번에는 E의 Collection이 아니라, E의 상위 타입의 Collection이면 될 것이다. 와일드 카드 타입 Collection<? super E>이 그런 의미이다.

public void popAll (Collection<? super E> dst) {
    while (! isEmpty())
        dst.add(pop())
}

유연성을 극대화하려면 원소의 생산자나 소비자용 입력 매개변수에 와일드 카드 타입을 사용하라.

한편, 입력 매개변수가 생산자와 소비자 역할을 동시에 한다면, 이때는 타입을 정확하게 지정해야하는 상황으로 와일드카드를 사용하지 말아야 한다.

언제 어떤 와일드 카드를 사용할까? (PECS)

PECS : Producer-extends, Consumer-super

즉, 매개변수화 타입 T가 생산자라면 <? extends E>를 사용하고, 소비자라면 <? super E>를 사용하라.

Stack의 예시를 다시 참조하자.

public void pushAll (Iterable<E> src) {
    for (E e : src)
        push(e); 
}

pushAll()의 src 매개변수는 Stack이 사용할 E 인스턴스를 생산하므로, src의 적절한 타입은 Iterable<? extends E> 이다.

public void popAll (Collection<E> dst) {
    while (! isEmpty())
        dst.add(pop())
}

반면 popAll()의 dst 매개변수는 Stack의 E 인스턴스를 소비하므로, dst의 적절한 타입은 Collection<? super E> 이다.

PECS 공식은 와일드 카드 타입을 사용하는 기본 원칙이다. 이를 겟풋 원칙(Get and Put Principle) 이라고 부른다.

코드 30-2의 union() 을 보자.

    public static <E> Set<E> union(Set<E> s1, Set<E> s2) {
        ...
    }

s1, s2 모두 E의 생산자이므로 다음과 같이 선언해야 한다.

    public static <E> Set<E> union(Set<? extends E> s1, Set<? extends E> s2) {
        ...
    }

반환 타입은 여전히 Set임에 주목하자. 반환 타입에는 한정적 와일드카드 타입을 사용해서는 안 된다. 유연성을 높여주기는 커녕, 클라이언트 코드에서도 와일드카드 타입을 써야하기 때문이다.

만약 클래스 사용자가 와일드 카드 타입에 대해서 고민한다면 그 API에 문제가 있을 가능성이 크다.

앞의 코드는 자바 8 부터 제대로 컴파일된다. 자바 7까지는 타입 추론 능력이 충분히 강력하기 못해 문맥에 맞는 반환(목표) 타입을 명시해야 했다. 이런 경우에는 명시적 타입 인수로 타입을 알려주면 되었다.

Set<Number> numbers = Union.<Number>union(s1, s2);

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

타입 매개변수와 와일드카드에는 공통되는 부분이 있어서, 메서드를 정의할 때 둘 중 어느 것을 사용해도 좋을 때가 있다.

public static <E> void swap(List<E> list, int i, int j);
public static void swap(List<?> list, int i, int j);

public API라면 간단한 두 번째가 낫다. 어떤 리스트든 이 메서드에 넣으면 명시한 인덱스의 원소를 교환해 줄 것이다. 신경 써야할 타입 매개변수도 없다.

메서드 선언에 타입 매개변수가 한 번만 나오면 와일드카드로 교체하라. 이때 비한정적 타입 매개변수라면 비한정적 와일드카드로, 한정적 타입 매개변수라면 한정적 와일드카드로 바꿔라.

하지만 두 번째 swap 선언에는 문제가 있다.

public static void swap(List<?> list, int i, int j) {
    list.set(i, list.set(j, list.get(i)));
}

이 코드를 컴파일하면 방금 꺼낸 원소를 다시 리스트에 넣을 수 없다는 오류 메세지가 나온다. 원인은 List의 타입이 List<?> 인데, List<?>에는 null 외에는 어떠한 값도 넣을 수 없기 때문이다. 다행히 (런타임 오류를 낼 수 있는) 형변환이나 리스트의 로 타입을 사용하지 않고 해결하는 방법이 있다. 와일드카드 타입의 실제 타입을 알려주는 private 도우미 메서드로 작성하여 활용하는 것이다.

public static void swap(List<?> list, int i, int j) {
    swapHelper(list, i, j);
}

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

swapHelper()은 리스트가 List임을 알고 있으므로, 이 리스트에서 꺼낸 값이 항상 E 이고, 리스트에 다시 넣어도 안전함을 알고 있다. 이 덕에 swap() 메서드를 호출하는 클라이언드는 swapHelper()와 같이 복잡한 내부 존재는 모른 채 혜택을 누릴 수 있다.

매개변수(parameter)와 인수(argument)의 차이

매개변수는 메서드 선언에 정의한 변수, 인수는 메서드 호출시 넘어가는 '실제값'

  void add(int value) { ... }
  add(10)

여기서 value는 매개변수, 10은 인수이다.

제네릭으로 확장해보자.

class Set<T> { ... }
Set<Integer> = ... ;

여기서 T는 타입 매개변수, Integer는 타입 인수가 된다.

🍑 결론

  • extends가 썩 잘 어울리는 단어는 아니다. 하위 타입이란 자기 자신도 포함하지만, 그렇다고 자신을 확장한 것은 아니기 때문이다. (item 29)

  • 조금 복잡하더라도 와일드카드 타입을 적용하면 API가 훨씬 유연해진다. 널리 쓰일 라이브러리를 작성한다면 반드시 와일드카드 타입을 적절히 사용해야 한다.

  • PECS 원칙을 기억하자.

  • Comparable과 Comparator는 모두 소비자이다.


Referenced by

-

jseok0917 commented 3 months ago

네, 클래스를 만들 때에도 와일드카드(?)를 사용할 수 있습니다. 와일드카드는 제네릭 타입에서 사용되며, 타입 파라미터를 알 수 없는 경우에 유용하게 활용됩니다.

와일드카드는 보통 제네릭 클래스, 제네릭 메서드, 제네릭 인터페이스의 타입 파라미터로 사용됩니다. 와일드카드는 다음과 같은 세 가지 형태로 사용될 수 있습니다:

<?>: 모든 타입을 나타냅니다. 제네릭 클래스나 메서드에 와일드카드를 사용하면 해당 위치에 어떤 타입이든 사용할 수 있습니다.

<? extends T>: 상한 경계 와일드카드입니다. T의 하위 클래스들을 나타냅니다. 따라서 T 자신이나 T의 하위 클래스의 인스턴스만 사용할 수 있습니다.

<? super T>: 하한 경계 와일드카드입니다. T의 상위 클래스들을 나타냅니다. 따라서 T 자신이나 T의 상위 클래스의 인스턴스만 사용할 수 있습니다.

와일드카드는 제네릭 타입을 더 유연하게 만들어주며, 다양한 상황에서 유용하게 사용될 수 있습니다.