본문 바로가기
JAVA/Effective Java

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

by Garonguri 2022. 5. 17.
728x90

시작 하기 전에 : 매개변수화 타입 : 불공변 -> "서로 상-하위 타입의 관계가 아니다" 라는 것을 기억해야 한다.

List가 상-하위 타입이 아닌 이유는 다 알 것이라 생각.. 리스코프 치환 원칙에도 어긋나고, 말도 안된다.

 

그럼, 불공변 방식보다 유연한 타입은 없을까?

-> 한정적 와일드카드 타입!

"한정적 와일드카드 타입을 사용하지 않은 메서드"
public void pushAll(Iterable<E> src) {
        for (E e : src) {
            push(e);
        }
    }
"컴파일은 되지만, 결함 존재"
// Iterable src원소 타입이 스택의 원소 타입과 일치할 때만 잘 작동한다.
// 그렇지 않을 경우 매개변수화 타입은 불공변이기 때문에 imcompatible type error 발생
"한정적 와일드카드 타입 ?를 사용한 경우"
public void pushAll(Iterable<? extends E> src) {
	for (E e : src) {
		push(e);
	}
}

"Iterable<? extends E> : E의 자기 자신도 포함하는 하위 타입의 Iterable"
// 타입 안전하다.

"한정적 와일드카드 타입을 사용하지 않은 경우"
public void popAll(Collection<E> dst) {
	while (!isEmpty()) {
		dst.add(pop());
	}
}

"주어진 컬렉션 원소 타입이 스택 원소 타입과 일치할 때만 잘 작동된다."
"한정적 와일드카드 타입을 사용한 경우"
public void popAll(Collection<? super E> dst) {
	while (!isEmpty()) {
		dst.add(pop());
	}
}
"Collection<? super E> : E는 자기 자신을 포함하는 상위 타입의 Collection이어야 한다."

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

 

입력 매개변수가 생산자, 소비자 역할을 동시에 한다면? 타입을 정확히 지정해야 한다. -> 즉, 와일드 카드 타입은 쓰지 말 것

 

  • 생산자 : 입력 매개변수로부터 이 컬렉션으로 원소를 옮겨 담는다는 뜻
  • 소비자 : 컬렉션 인스턴스의 원소를 입력 매개변수로 옮겨 담는다는 뜻

어떤 와일드 카드 타입을 사용해야 할까?

와일드 카드 타입을 사용하는 기본 원칙 (겟 풋 원칙 이라고도 부른다.)

PECS(Producer-Extends, Consumer-Super)

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

 

* 주의 사항 *

반환 타입에는 한정적 와일드카드 타입을 사용하면 안된다.

- 유연성을 높여주는 역할을 하지 못한다.

- 클라이언트 코드에서 와일드 카드 타입을 사용/신경써야 하기 때문에 문제가 있는 API가 될 가능성이 커진다.

 


컴파일러가 올바른 타입을 추론하지 못할 때는 언제든지 명시적 타입 인수를 사용해 타입을 알려주자.


Comparable<E>는 E 인스턴스를 소비한다. 또한, 선후 관계를 뜻하는 정수를 생산한다.

따라서 매개변수화 타입보다는 한정적 와일드카드 타입으로 대체하는 것이 낫다.

 

Comparable은 언제나 소비자이므로, Comparable<E> 보다는 Comparable<? super E>를 사용하자.

Comparator은 언제나 소비자이므로, Comparator<E> 보다는 Comparator<? super E>를 사용하자.

 

Comparable 또는 Comparator를 직접 구현하지 않고,

'직접 구현한 다른 타입을 확장한 타입'을 지원하기 위해서는 와일드카드 타입이 필요하다.

 


타입 매개변수와 와일드드 타입은 공통되는 부분이 있어 메서드를 정의할 때 둘 중 아무거나 사용해도 괜찮은 경우가 있다.

그러나 더 좋은건 분명히 있다.

"비한정적 타입 매개변수 사용"
public static <E> void swap(List<E> list, int i, int j);


"비한정적 와일드카드 사용"
public static void swap(List<?> list, int i, int j);
// public API인 경우에 더 낫다.
// 어떤 리스트던 이 메서드에 넘기면 명시한 인덱스 원소들을 교환해 줄 것이고,
// 신경써야 할 타입 매개변수도 없기 때문이다.

* 기본 규칙 *

메서드 선언에 타입 매개변수가 한 번만 나오면 와일드 카드로 써라.

 

-> (비)한정적 타입 매개변수라면 (비)한정적 와일드카드 타입으로 바꾸어서 구현하라는 의미이다.

  • 한정적 와일드카드 : 특정 타입을 제한. extend, super ..
  • 비한장적 와일드카드 : 어떤 타입이 오던 관계가 없다는 뜻. <?>
"비한정적 와일드카드 사용"
-> 오류가 있는 코드. List<?>에는 null 외에는 어떤 값도 넣을수가 없기 때문에 생긴다.
public static void swap(List<?> list, int i, int j){
	list.set(i, list.set(j, list.get(i))); 	
}



/*----------오류 해결 : 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<E>라는 것을 알고 있다.
	//즉, 리스트에서 꺼낸 값의 타입이 항상 E이고 E타입의 값을 넣으면 안전하다는 것을 알고 있다.
    list.set(i, list.set(j, list.get(i))); 
}

 

 

/* 핵심 정리 */
널리 쓰일 라이브러리를 작성한다면, 유연한 API를 위해 와일드카드 타입을 사용하자.
생산자는 extends, 소비자는 super을 사용한다.
728x90

댓글