제네릭 - 규칙 28 한정적 와일드 카드를 써서 API 유연성을 높여라
JAVA/Effective Java

제네릭 - 규칙 28 한정적 와일드 카드를 써서 API 유연성을 높여라

반응형


List<String>와 같은 형인자 자료형은 불변 자료형이다.

이는 저번 규칙에서 보았듯이 List<String>은 List<Object>의 어떠한 하위 자료형도 아니라는 것을 확인 하였다.

하지만 가끔은 불변 자료형보다 높은 유연성이 필요할 때가 있다.

예를 들어 일련의 원소들을 인자로 받아 차례로 스택에 집어 넣는 메서드가 있다고 가정하여 보자.



1
2
3
4
5
6
7
8
9
10
11
12
13
14
package rule28;
 
import java.util.Stack;
 
public class Rule28 <E> extends Stack<E>{
 
    private static final long serialVersionUID = 1L;
    
    public void pushAll(Iterable<E> src) {
        for (E e : src) {
            push(e);
        }
    }
}
cs



위와 같이 Stack을 상속받아 구현한 Rule28 클래스를 
다음과 같이 Number 자료형으로 선언 후 그 하위 자료형에 속하는 Integer list를 이용하여 pushAll 메소드를 호출하려 할 때, 가능할 것 같지만 오류가 발생한다.



1
2
3
4
5
6
7
public class Main {
    public static void main(String args[]) {
        Rule28<Number> numberStack = new Rule28<>();
        List<Integer> iter = Arrays.asList(1551);
        numberStack.pushAll(iter);
    }
}
cs




Exception in thread "main" java.lang.Error: Unresolved compilation problem:  
The method pushAll(Iterable<Number>) in the type Rule28<Number> is not applicable for the arguments (List<Integer>) 

at rule28.Main.main(Main.java:11) 


왜냐하면 형인자 자료형은 불변이기 때문이다. 
Iterable<Number>는 List<Integer>의 하위 자료형도 상위 자료형도 아니기 때문인다.


이러한 문제를 해결하기 위해서는 한정자 와일드카드 자료형이라는 특별한 형인자 자료형을 사용하면 해결 할 수 있다.



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 변경된 Rule28 클래스의 pushAll 클래스
public void pushAll(Iterable<extends E> src) {
    for (E e : src) {
        push(e);
    }
}
 
// 아무런 오류가 사라지는 것을 확인 할 수 있다.
public class Main {
    public static void main(String args[]) {
        Rule28<Number> numberStack = new Rule28<>();
        List<Integer> iter = Arrays.asList(1551);
        numberStack.pushAll(iter);
    }
}
cs



현재 메소드에 전달되는 파라미터의 자료형이 E의 하위 자료형이라는 것을 명시해 줄 경우에는 
다음과 같이 Number 타입에 Integer 자료형을 넘길 수 있다.

다른 예를 들어보자.

만약 전달받은 콜렉션에 현재 Rule28에 들어있는 E타입에 데이터를 전달할 경우는 어떨까?



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
 * 전달 받은 Collection dst에 스택에 쌓인 데이터를 반환
 * 
 * @param dst
 */
public void popAll(Collection<E> dst) {
    while(!isEmpty()) {
        dst.add(pop());
    }
}
 
public class Main {
    public static void main(String args[]) {
        Rule28<Number> numberStack = new Rule28<>();
        List<Object> ad = new ArrayList<>();
        numberStack.popAll(ad);   // 오류 발생
    }
}
cs



Exception in thread "main" java.lang.Error: Unresolved compilation problem:  
The method popAll(Collection<Number>) in the type Rule28<Number> is not applicable for the arguments (List<Object>) 

at rule28.Main.main(Main.java:10) 


이 문제도 마찬가지로 
Number 타입으로 선언된 Rule28은 Object 자료형을 사용하는 컬렉션과 하위 자료형 또는 상위자료형도 아니기 때문에 오류가 발생한다.

이런 경우에

넘어온 데이터의 자료형을 다음과 같이 선언해주면 문제가 없다.

왜냐하면 전달받은 Object 자료형의 경우 Number의 상위 자료형이기 때문이다.


1
2
3
4
5
6
7
8
9
10
/**
    * 전달 받은 Collection dst에 스택에 쌓인 데이터를 반환
    * 
    * @param dst
    */
public void popAll(Collection<super E> dst) {
    while(!isEmpty()) {
        dst.add(pop());
    }
}
cs





위와 같은 상황들을 살펴보았을 때 다음과 같이 정리 될 수 있다.

1. 자료형이 똑같은 경우 와일드 카드 자료형을 사용할 필요없다.
2. 메서드의 인자(파라미터로 넘어온 인자)가 생자인경우 <? extends E>를 사용하고 소비자인 경우 <? super E>를 사용한다.

PECS (Produce - Extends, Consumer - Super)

해당 사항에 대해 예를 들어보자.

Comparable, Comparator은 언제나 소비자 이므로 

Comparable<? super T>를 사용해야 한다.

// Collection Sort 
default void sort(Comparator<? super E> c) {
 }


[주의사항]

1. 반환값에는 와일드카드 자료형을 쓰면 안된다.

위에서 보면 Rule28 클래스와 같이 
다른 개발자들이 사용해야 하는 클래스의 와일드 카드 자료형이 어떤 것인지 고민을 해야한다면
그 API는 설계가 잘못 된것이다.

맞지않는 자료형은 거부하고, 맞는 자료형만 받을 수 있도록 제공되어야 한다.

2. 전달 받은 인자가 와일드 카드일 때 주의사항



1
2
3
public static <T> void setData(List<extends T> list) {
    Iterator<T> i = list.iterator();
}
cs



다음과 같은 코드가 있다고 가정해보자.
 
위 코드는 컴파일 시점부터 다음과 같은 오류를 출력한다.






그 이유는 보면 알겠지만
list의 iterator 메소드의 반환값이 Iterator<T>를 반환하는 것이 아닌
Iterator<? extends T>를 반환하기 때문이다.

그래서 다음과 같이 변경해주면 된다.



1
2
3
public static <T> void setData(List<extends T> list) {
    Iterator<extends T> i = list.iterator();
}
cs



3. 형인자와 와일드 카드사이에 존재하는 이원성 문제

swap 메소드를 사용할 때 다음과 같이 사용한다.
public static void swap(List<?> list, int i, int j);

하지만 다음과 같은 경우의 문제가 있을 수 있기에 주의해야 한다. 




1
2
3
4
5
6
7
8
public static void swap(List<?> list, int i, int j) {
    list.set(i,  list.set(j,  list.get(i)));
}
 
// 오류 내용
Description    Resource    Path    Location    Type
The method set(int, capture#3-of ?) in the type List<capture#3-of ?> 
is not applicable for the arguments (int, capture#4-of ?)
cs



저번 규칙23번에서 다루었듯이 와일드 카드 자료형의 경우 null이외에 데이터를 넣을 수 없다는 것을 알아야 한다.

그래서 이를 해결해주기 위해서는 제네릭 메서드를 도입 해야 한다.



1
2
3
4
5
6
7
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)));
}
cs



출처 : 조슈아 블로크, 『 Effective Java 2/E』, 이병준 옮김, 인사이트(2014.9.1), 규칙28 인용.

반응형