Search

Bounded Wild Card Type

Created
2023/08/19 00:22

1. <? extends E>

임의의 타입을 지원하기 위해서 사용하는 것이 제네릭 타입이다. 단순 타입 T만을 사용했을 때 제네릭이 만족될 수도 있지만, 막상 프로그래밍을 하다보면 그렇지 않을 때가 있다. 예를 들어 아래와 같은 스택이 있다고 해보자.
public class Stack<E> { public Stack(); public void push(E e); public E pop(); public boolean isEmpty(); }
Java
복사
그리고 이 커스텀 스택은 여러 값들을 한 번에 밀어 넣을 수 있는 pushAll도 지원한다고 해보자.
public void pushAll(Iterabled<E> src) { for (E e : src) { push(e); } }
Java
복사
완벽해 보이는 위 코드는 생각만큼 잘 작동하지 않는다. 예를 들어 위 커스텀 스택을 이용하여 숫자 타입 (Integer, Double)등을 담을 수 있도록 아래처럼 생성하고, 여러 Integer 값들을 추가하려고 해보자.
int[] arr = {1, 2, 3, 4, 5}; Stack<Number> st = new Stack<>(); Iterable<Integer> iter = Arrays.stream(arr).iterator(); st.pushAll(iter);
Java
복사
위 코드는 개념 상 Number를 담을 수 있는 스택이기에 정수 값들을 잘 밀어 넣어야할 것 같지만, Iterable<Integer> cannot be converted to Iterable<Number>라는 예외를 던지게 된다. 임의의 제네릭 타입이 명확하게 매개변수화 타입이 되면, 이는 Invariant이기 때문이다. 즉, Stack<Number>는 Iterable<Number>만 받을 수 있기에, Iterable<Integer>는 이용할 수 없게 된다. 이런 문제를 해결할 수 있는 것이 Bounded Wild Card Type이 되겠다.
이전에 작성했던 Iterable<E>를 Iterable<? extends E>라고 명시하게 되면, E타입의 하위 타입을 모두 Iterable 인자로 받아낼 수 있게 된다.
public void pushAll(Iterable<? extends E> src) { for (E e : src) { push(e); } }
Java
복사

2. <? super E>

그럼 모두 문제가 해결되었을까 싶지만, 그렇지 않다. 모든 요소를 반환하는 popAll을 구현해보자.
public void popAll(Collection<E> dst) { while (!isEmpty()) { dst.add(pop()); } }
Java
복사
이와 같은 메서드를 아래 코드처럼 컬렉션에 추출하는 코드를 보자.
int[] arr = {1, 2, 3, 4, 5}; Stack<Number> st = new Stack<>(); Iterable<Integer> iter = Arrays.stream(arr).iterator(); st.pushAll(iter); Collection<Object> collection = List.of(-1, 0); st.popAll(collection);
Java
복사
이 코드 역시 기대한대로 값을 dst에 뽑아내는 식으로 동작해야할 것 같지만, Object는 Number의 상위 타입인 이유로 예외를 던지게 된다.
따라서 아래와 같이 기존의 Collection<E>를 Collection<? super E>라는 형태로 바꾸게 되면, 정상적으로 동작하는 것을 볼 수 있다.
public void popAll(Collection<? super E> dst) { while (!isEmpty()) { dst.add(pop()); } }
Java
복사

3. 이용 목적 및 방법

Bounded Wild Card Type의 이용 목적은 코드를 통해서 본 것처럼 자신이 작성한 API의 유연성을 높이는데 있다.
코드에서 느낀 것처럼 무언가를 생산해내는 입장이라면 extends를 활용 (pushAll)하고, 무언가를 소모하는 입장이라면 super를 활용 (popAll)을 사용하면 되겠다.
만일 메서드가 생산자와 소비자의 입장을 둘 다 갖고 있다면, 이는 정확한 타입을 요구하는 것이므로 Bounded Wild Card Type을 사용하지 않는 것이 좋다.
관련해서는 max와 swap 등의 함수를 찾아보면 도움이 많이 된다.
max의 경우, 비교 연산을 이용하기 때문에 Comparable의 명시가 필요하다. 따라서 E 타입은 비교 가능한 Comparable 타입이어야 되고, 이 때의 Comparable은 E의 상위 타입이기만 하면 된다.
swap의 경우, 임의의 타입을 받아내기 위해 List<?>를 사용한 것을 볼 수 있다. 하지만 List<?>에는 null 값만 넣을 수 있기 때문에 정상적인 swap이 되지 않는 것을 볼 수 있다. 이와 같은 문제를 해결하기 위해 private인 Helper 메서드를 두고 List<E>와 같은 구체적인 타입을 이용하도록 한다.
public static <E extends Comparable<? super E>> E max(List<? extends E>) { // ... }
Java
복사
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))); }
Java
복사

4. 비고

Bounded Type Parameter → E extends T / E super T
Bounded Wild Card Type → ? extends T / ? super T
Unbounded Type Parameter → E
Unbounded Wild Card Type → ?