이펙티브 자바 아이템 31 - 한정적 와일드카드를 사용해 API 유연성을 높이라 - 핵심 정리
아이템 31 - 한정적 와일드카드를 사용해 API 유연성을 높이라 - 핵심 정리
이 글은 백기선 님의 이펙티브 자바 강의와 이펙티브 자바 3 / E 편을 참고하여 작성하였습니다.
Chooser 와 Union API 개선
// 와일드카드 타입을 이용해 대량 작업을 수행하는 메서드를 포함한 제네릭 스택 (181-183쪽)
public class Stack<E> {
private E[] elements;
private int size = 0;
private static final int DEFAULT_INITIAL_CAPACITY = 16;
// 코드 29-3 배열을 사용한 코드를 제네릭으로 만드는 방법 1 (172쪽)
// 배열 elements는 push(E)로 넘어온 E 인스턴스만 담는다.
// 따라서 타입 안전성을 보장하지만,
// 이 배열의 런타임 타입은 E[]가 아닌 Object[]다!
@SuppressWarnings("unchecked")
public Stack() {
elements = (E[]) new Object[DEFAULT_INITIAL_CAPACITY];
}
public void push(E e) {
ensureCapacity();
elements[size++] = e;
}
public E pop() {
if (size==0)
throw new EmptyStackException();
E result = elements[--size];
elements[size] = null; // 다 쓴 참조 해제
return result;
}
public boolean isEmpty() {
return size == 0;
}
private void ensureCapacity() {
if (elements.length == size)
elements = Arrays.copyOf(elements, 2 * size + 1);
}
// 코드 31-2 E 생산자(producer) 매개변수에 와일드카드 타입 적용 (182쪽)
public void pushAll(Iterable<E> src) {
for (E e : src)
push(e);
}
// 코드 31-3 와일드카드 타입을 사용하지 않은 popAll 메서드 - 결함이 있다! (183쪽)
// public void popAll(Collection<E> dst) {
// while (!isEmpty())
// dst.add(pop());
// }
// 코드 31-4 E 소비자(consumer) 매개변수에 와일드카드 타입 적용 (183쪽)
public void popAll(Collection<E> dst) {
while (!isEmpty())
dst.add(pop());
}
}
지금까지는 Stack<E>
를 통해 오직 한 타입만을 지칭해 사용했다.
제네릭은 공변 하지 않기 때문에 그 타입의 상속구조와는 관계없이 하나의 타입만 지칭하게 된다.
만약 Stack<Number>
에 Integer
객체를 넣고 싶다면 넣을 수가 없게된다.
그러나 Stack<Number>
에 Integer
객체를 넣는 행위는 위험한 행위가 아니다.
해당하는 객체의 상위 인터페이스로 사용 하는 건 전혀 문제가 되지 않는다.
// 코드 31-2 E 생산자(producer) 매개변수에 와일드카드 타입 적용 (182쪽)
public void pushAll(Iterable<E> src) {
for (E e : src)
push(e);
}
그러나 pushAll(Iterable<E> src)
메서드의 매개변수로 E
타입을 정의했기 때문에
Stack<Number>
에서 Integer
객체를 pushAll
메서드의 매개변수로 전달할 수 없다.
여기서 한정적 와일드 카드 사용하면 어떻게 될까?
한정적 타입 은 Iterable<E extends Number>
와 같이 E
라는 어떠한 한정적인 타입을 지칭하고
한정적 와일드 카드 는 Iterable<? extends Number>
와 같이 어떠한 타입을 지칭하지 않는다.
<? extends Number>
는 Number 클래스를 상속받은 아무 타입 을 의미한다.
// 코드 31-2 E 생산자(producer) 매개변수에 와일드카드 타입 적용 (182쪽)
public void pushAll(Iterable<? extends E> src) {
for (E e : src)
push(e);
}
pushAll
메서드의 매개변수를 한정적 와일드 카드 로 변경한다.
E
타입을 상속한 아무 타입의 Iterable
을 매개변수로 받을 수 있다.
// 와일드카드 타입을 이용해 대량 작업을 수행하는 메서드를 포함한 제네릭 스택 (181-183쪽)
public class Stack<E> {
private E[] elements;
private int size = 0;
private static final int DEFAULT_INITIAL_CAPACITY = 16;
// 코드 29-3 배열을 사용한 코드를 제네릭으로 만드는 방법 1 (172쪽)
// 배열 elements는 push(E)로 넘어온 E 인스턴스만 담는다.
// 따라서 타입 안전성을 보장하지만,
// 이 배열의 런타임 타입은 E[]가 아닌 Object[]다!
@SuppressWarnings("unchecked")
public Stack() {
elements = (E[]) new Object[DEFAULT_INITIAL_CAPACITY];
}
public void push(E e) {
ensureCapacity();
elements[size++] = e;
}
public E pop() {
if (size==0)
throw new EmptyStackException();
E result = elements[--size];
elements[size] = null; // 다 쓴 참조 해제
return result;
}
public boolean isEmpty() {
return size == 0;
}
private void ensureCapacity() {
if (elements.length == size)
elements = Arrays.copyOf(elements, 2 * size + 1);
}
// 코드 31-2 E 생산자(producer) 매개변수에 와일드카드 타입 적용 (182쪽)
public void pushAll(Iterable<? extends E> src) {
for (E e : src)
push(e);
}
// 코드 31-3 와일드카드 타입을 사용하지 않은 popAll 메서드 - 결함이 있다! (183쪽)
public void popAll(Collection<E> dst) {
while (!isEmpty())
dst.add(pop());
}
// 제네릭 Stack을 사용하는 맛보기 프로그램
public static void main(String[] args) {
Stack<Number> numberStack = new Stack<>();
Iterable<Integer> integers = Arrays.asList(3, 1, 4, 1, 5, 9);
numberStack.pushAll(integers);
}
}
한정적 와일드카드 타입 을 사용하면 Stack<Number>
에 pushAll
메서드를 통해 Integer
를 넣을 수 있게된다.
Iterable<? extends E>
를 풀어보면 E
는 Number
클래스가 되고
Iterable<? extends E>
는 결국 Number
클래스를 상속받은 모든 타입을 의미하므로 Integer
를 매개변수로 전달할 수 있는 것이다.
이렇게 어떤 매개변수를 받아 어딘가에 쌓아두는 역할 을 하는 것을 생산자 혹은 프로듀서 라고 한다.
이러한 경우에는 객체를 쌓아두는 컨테이너의 타입보다 하위 타입 을 넣어줄 수 있다.
전달받은 객체보다 상위타입으로 사용하기 떄문에 컨테이너의 타입보다 하위 타입 을 넣어주어도 안전하기 떄문이다.
ppublic class Main {
public static void main(String[] args) {
Stack<Number> numberStack = new Stack<>();
Iterable<Integer> integers = Arrays.asList(3, 1, 4, 1, 5, 9);
numberStack.pushAll(integers);
Iterable<Double> doubles = Arrays.asList(3.1, 1.0, 4.0, 1.0, 5.0, 9.0);
numberStack.pushAll(doubles);
}
}
Number
의 하위타입을 전부 넣을 수 있으므로 위와같이 Double
타입도 넣을 수 있게된다.
결국 Double
을 Number
타입으로 사용하게되기 때문에 아무런 문제가 발생하지 않는다.
어떤 매개변수를 받아 어딘가에 쌓아두는 역할 을 하는 것을 생산자 에서는 <? extends E>
와 같이
extends
를 활용할 수 있다.
한정적 와일드카드 타입 을 사용하면 API 의 유연성이 높아진다.
반대로 컨테이너 안에 들어있는 객체를 꺼낼 때 는 해당 타입보다 높은 타입 으로 꺼낼 수 있다.
public class Main {
public static void main(String[] args) {
Stack<Number> numberStack = new Stack<>();
Iterable<Integer> integers = Arrays.asList(3, 1, 4, 1, 5, 9);
numberStack.pushAll(integers);
Iterable<Double> doubles = Arrays.asList(3.1, 1.0, 4.0, 1.0, 5.0, 9.0);
numberStack.pushAll(doubles);
Collection<Object> objects = new ArrayList<>();
numberStack.popAll(objects); // 컴파일 에러
}
}
위처럼 Stack<Number>
에 있는 값을 꺼내 Collection<Object>
에 넣어주고 싶지만 컴파일 에러가 발생한다.
public void popAll(Collection<E> dst) {
while (!isEmpty())
dst.add(pop());
}
popAll
메서드의 매개변수가 Collection<E>
로 타입 하나로 선언되어있기 떄문에
더 상위 타입인 Object
와는 다른 타입인 것이다.
Stack<Number>
안에 있는 값을 Collection<Object>
에서 사용해도 문제가 되지 않는다.
Number
보다 상위타입인 Object
타입으로 사용하기 때문이다.
public void popAll(Collection<? super E> dst) {
while (!isEmpty())
dst.add(pop());
}
Collection<? super E>
와 같이 super
를 사용한 한정적 와일드 카드 타입 을 사용하여 해결한다.
<? extends E>
와 같이 상위 타입이 한정되어 있는 경우는 상위 한정 이라고 부르고,
<? super E>
는 하위 타입이 한정되어 있어서 하위 한정 이라고 부른다.
<? super E>
는 E
타입의 상위 타입들을 의미한다.
예를들어 <? super Integer>
라면 Integer
의 상위 타입인 Number
나 Object
를 허용한다.
부모 클래스 혹은 그 클래스가 구현하고 있는 인터페이스 타입들을 허용하는 것이다.
public class Main {
public static void main(String[] args) {
Stack<Number> numberStack = new Stack<>();
Iterable<Integer> integers = Arrays.asList(3, 1, 4, 1, 5, 9);
numberStack.pushAll(integers);
Iterable<Double> doubles = Arrays.asList(3.1, 1.0, 4.0, 1.0, 5.0, 9.0);
numberStack.pushAll(doubles);
Collection<Object> objects = new ArrayList<>();
numberStack.popAll(objects);
}
}
Object
클래스는 Number
의 상위 타입이므로 popAll
메서드의 사용이 가능해진다.
Number
를 Object
컬렉션에 넣어도 아무 문제가 없다.
Object
가 Number
의 상위 타입이기 때문이다.
이렇게 한정적 와일드 카드 타입 을 사용해 API 의 유연성을 높일 수 있다.
여기서 규칙을 찾을 수 있는데
프로듀서 (뭔가를 받아와서 쌓아넣는 곳, add 나 push 같은), 생산자 에서는 <? extends E>
를 사용하고,
컨슈머 (내가 가지고 있는 것을 꺼내 전달하는 곳) 에서는 <? super E>
를 사용한다.
프로듀서는 extends, 컨슈머는 super 의 앞글자만 따서 PECS 라고 부른다.
PECS: Producer-Extends, Consumer-Super
// T 생산자 매개변수에 와일드카드 타입 적용 (184쪽)
public class Chooser<T> {
private final List<T> choiceList;
private final Random rnd = new Random();
// 코드 31-5 T 생산자 매개변수에 와일드카드 타입 적용 (184쪽)
public Chooser(Collection<> choices) {
choiceList = new ArrayList<>(choices);
}
public T choose() {
return choiceList.get(rnd.nextInt(choiceList.size()));
}
}
Chooser
클래스이다.
public Chooser(Collection<T> choices) {
choiceList = new ArrayList<>(choices);
}
생성자에서 어떠한 Collection
을 받아서 랜덤한 값을 꺼내주는 기능을 한다.
어떠한 값을 가져와 추가하는 것이기 때문에 프로듀서 에 해당한다.
프로듀서 에서 위와 같이 T
라는 하나의 타입만 정의를 하면 선언한 그 타입만 사용해야한다.
public class Main {
public static void main(String[] args) {
List<Integer> intList = List.of(1, 2, 3, 4, 5, 6);
Chooser<Number> chooser = new Chooser<>(intList);
for (int i = 0; i < 10; i++) {
Number choice = chooser.choose();
System.out.println(choice);
}
}
}
위와 같이 Chooser<Number>
로 선언했기 때문에
List<Integer>
타입은 넘길 수 없다.
그러나 Number
의 하위 타입인 Integer
타입의 값을 할당해도 문제가 발생하지 않는다.
Integer
를 Number
타입으로 사용하기 때문이다.
이 반대인 Number
을 Integer
타입으로 사용할 수 없다.
더 추상적인 타입을 구체화된 타입으로 사용할 수 없다.
public Chooser(Collection<? extends T> choices) {
choiceList = new ArrayList<>(choices);
}
위와같이 프로듀서 에 extends
키워드를 사용하면 아무런 무리없이
Nuumber
의 하위 타입인 Integer
타입의 List
를 받을 수 있다.
// 코드 30-2의 제네릭 union 메서드에 와일드카드 타입을 적용해 유연성을 높였다. (185-186쪽)
public class Union {
public static <E> Set<E> union(Set<E> s1,
Set<E> s2) {
Set<E> result = new HashSet<>(s1);
result.addAll(s2);
return result;
}
}
두 개의 Set
을 합쳐주는 Union
클래스이다.
public class Main {
// 향상된 유연성을 확인해주는 맛보기 프로그램 (185쪽)
public static void main(String[] args) {
Set<Integer> integers = new HashSet<>();
integers.add(1);
integers.add(3);
integers.add(5);
Set<Double> doubles = new HashSet<>();
doubles.add(2.0);
doubles.add(4.0);
doubles.add(6.0);
Set<Number> numbers = union(integers, doubles);
// // 코드 31-6 자바 7까지는 명시적 타입 인수를 사용해야 한다. (186쪽)
// Set<Number> numbers = Union.<Number>union(integers, doubles);
System.out.println(numbers);
}
}
Set<Integer>
과 Set<Double>
을 Set<Number>
으로 합쳐도 전혀 문제가 되지 않는다.
// 코드 30-2의 제네릭 union 메서드에 와일드카드 타입을 적용해 유연성을 높였다. (185-186쪽)
public class Union {
public static <E> Set<E> union(Set<E> s1,
Set<E> s2) {
Set<E> result = new HashSet<>(s1);
result.addAll(s2);
return result;
}
}
하지만 현재는 E
라는 하나의 타입만 사용하고 있기 때문에 불가능하다.
union
메서드는 무언가를 받아와 넣어주는 역할을 하기 때문에 프로듀서 이다.
// 코드 30-2의 제네릭 union 메서드에 와일드카드 타입을 적용해 유연성을 높였다. (185-186쪽)
public class Union {
public static <E> Set<E> union(Set<? extends E> s1,
Set<? extends E> s2) {
Set<E> result = new HashSet<>(s1);
result.addAll(s2);
return result;
}
}
위처럼 extends
를 사용한 한정적 와일드카드 타입 을 사용해 API 를 더 유연하게 사용할 수 있다.
// 재귀적 타입 한정을 이용해 상호 비교할 수 있음을 표현 (179쪽)
public class RecursiveTypeBound {
// 코드 30-7 컬렉션에서 최댓값을 반환한다. - 재귀적 타입 한정 사용 (179쪽)
public static <E extends Comparable<E>> E max(Collection<E> c) {
if (c.isEmpty())
throw new IllegalArgumentException("컬렉션이 비어 있습니다.");
E result = null;
for (E e : c)
if (result == null || e.compareTo(result) > 0)
result = Objects.requireNonNull(e);
return result;
}
public static void main(String[] args) {
List<String> argList = List.of("keesun", "whiteship");
System.out.println(max(argList));
}
}
RecursiveTypeBound
클래스는 가장 큰 값을 꺼내주는 기능을한다.
이때 재귀적 타입 한정 을 사용했다.
여기서 좀 더 유연하게 만들려면
public static <E extends Comparable<? super E>> E max(List<? extends E> list) {
if (list.isEmpty())
throw new IllegalArgumentException("빈 리스트");
E result = null;
for (E e : list)
if (result == null || e.compareTo(result) > 0)
result = e;
return result;
}
위와 같이 값을 받아오는 프로듀서 애서는 List<? extends E>
를 사용하고
Comparable
은 무언가 값을 꺼내 비교하는 소비자 이기 떄문에
Comparable<? super E>
를 사용했다.
우리가 사용하는 타입의 상위 타입만 Comparable
을 구현한 경우가 있기 때문이다.
public class Box<T extends Comparable<T>> implements Comparable<Box<T>> {
protected T value;
public Box(T value) {
this.value = value;
}
public void change(T value) {
this.value = value;
}
@SuppressWarnings("unchecked")
@Override
public int compareTo(Box anotherBox) {
return this.value.compareTo((T)anotherBox.value);
}
@Override
public String toString() {
return "Box{" +
"value=" + value +
'}';
}
}
Comparable
을 구현한 Box
클래스를 작성한다.
public class IntegerBox extends Box<Integer> {
private final String message;
public IntegerBox(int value, String message) {
super(value);
this.message = message;
}
@Override
public String toString() {
return "IntegerBox{" +
"message='" + message + '\'' +
", value=" + value +
'}';
}
}
IntegerBox
클래스는 Box
클래스만 상속받고 있다.
IntegerBox
클래스는 직접적으로 Comparable
구현하고 있지는 않다.
public class Main {
public static void main(String[] args) {
List<IntegerBox> list = new ArrayList<>();
list.add(new IntegerBox(10, "keesun"));
list.add(new IntegerBox(2, "whiteship"));
System.out.println(RecursiveTypeBound.max(list));
}
}
이러한 경우에도 max
메서드가 잘 동작한다.
IntegerBox
클래스는 직접적으로 Comparable
구현하고 있지는 않지만
IntegerBox
의 super
타입인 Box
타입이
Comparable
구현했기 때문에 <E extends Comparable<? super E>>
타입에 맞는 타입이다.
와일드 카드 활용 팁
와일드 카드를 언제 어떻게 사용하면 좋은지와 주의사항을 알아보도록 하자.
와일드 카드는 ?
를 지칭한다.
?
하나만 사용하지는 않고 더 허용하고 싶은 매개변수를 넓혀줄 때
extends
나 super
키워드와 같이 와일드 카드 를 사용한다.
?
하나만 선언했을 때는 불특정 타입 이 된다.
E
는 특정할 수 있는 타입이지만 ?
는 특정할 수 없는 임의의 타입이다.
// 와일드카드 타입을 실제 타입으로 바꿔주는 private 도우미 메서드 (189쪽)
public class Swap {
public static <E> void swap(List<E> list, int i, int j) {
list.set(i, list.set(j, list.get(i)));
}
// 와일드카드 타입을 실제 타입으로 바꿔주는 private 도우미 메서드
private static <E> void swapHelper(List<E> list, int i, int j) {
list.set(i, list.set(j, list.get(i)));
}
public static void main(String[] args) {
// 첫 번째와 마지막 인수를 스왑한 후 결과 리스트를 출력한다.
List<String> argList = Arrays.asList(args);
swap(argList, 0, argList.size() - 1);
System.out.println(argList);
}
}
위의 클래스처럼 E
를 사용하는 경우에는 ?
로 고칠 수 있다.
// 와일드카드 타입을 실제 타입으로 바꿔주는 private 도우미 메서드 (189쪽)
public class Swap {
public static void swap(List<?> list, int i, int j) {
list.set(i, list.set(j, list.get(i))); // 컴파일 에러
}
// 와일드카드 타입을 실제 타입으로 바꿔주는 private 도우미 메서드
private static <E> void swapHelper(List<E> list, int i, int j) {
list.set(i, list.set(j, list.get(i)));
}
public static void main(String[] args) {
// 첫 번째와 마지막 인수를 스왑한 후 결과 리스트를 출력한다.
List<String> argList = Arrays.asList(args);
swap(argList, 0, argList.size() - 1);
System.out.println(argList);
}
}
위와같이 ?
를 사용하게 되면 값을 넣을 수가 없어진다.
List<?> list
와 같이 비 한정적 타입 을 사용하면
list.set(j, list.get(i))
에서도 비 한정적 타입 을 원한다.
심지어 list.get(i)
에서도 우리가 모르는 무언가의 타입이 나오게된다.
비 한정적 와일드카드 타입 은 타입을 모른다.
E
는 어떠한 특정한 타입을 안다.
?
타입은 오로지 Null
밖에 허용하지 않는다.
때문에 ?
타입 하나만 사용하는 걸 권장하지는 않는다.
?
를 사용했을 때 메서드 시그니쳐는 간소화시킬 수 있지만
값을 넣는 것 은 가능하지 않다.
대신 값을 꺼내는 것 은 가능하다.
?
타입은 Null
빼고는 아무것도 매치가 되지 않는다.
그래서 무언가를 꺼내기만 할 거라면 ?
하나만 사용해도 되지만
무언가를 넣는 작업이 있다면 ?
하나만 사용하면 안된다.
// 와일드카드 타입을 실제 타입으로 바꿔주는 private 도우미 메서드 (189쪽)
public class Swap {
public static void swap(List<?> list, int i, int j) {
swapHelper(list, i, j);
}
// 와일드카드 타입을 실제 타입으로 바꿔주는 private 도우미 메서드
private static <E> void swapHelper(List<E> list, int i, int j) {
list.set(i, list.set(j, list.get(i)));
}
}
무언가를 넣는 작업을 한다면 결국 E
타입을 정의해 사용할 수 밖에 없다.