언어/Effective Java

이펙티브 자바 Item31 한정적 와일드 카드를 사용해 API의 유연성을 높여라

now0204 2024. 6. 5. 09:42

한정적 와일드 카드를 써야 하는 이유 

  • 상속을 고려해서 제네릭을 짜야할 때 한정적 와일드 카드를 써야한다. 
  • 제네릭은 기본적으로 불공변이다. List<Object>와 List<String>은 서로 아무런 관계가 없는 타입이다.
  • 때로는 이러한 불공변을 보다 더 유연한 방식으로 사용해야할 때가 있다. 이때 와일드 카드를 사용한다.
public void pushAll(Iterable<E> src) {
	for (E e : src) {
    	push(e);
    }
}
  • 위 코드는 아무런 문제가 없다만, 
  • Stack을 Number로 선언하고, ArrayList<Integer>를 매개변수로 전달하면, Number가 Integer의 부모이므로, 논리적으로 될 것 같지만 동작하지 않는다.
  • 이럴때 한정적 와일드 카드로 해결할 수 있다.
public void pushAll(Iterable<? extends E> iterable) {
    for (E e : iterable) {
        push(e);
    }
}
  • 이와 같이 한정적 와일드 카드를 사용해 해결할 수 있다. 

 

비한정적 와일드카드 적용 방법

 

<? extends E>를 통해 와일드 카드를 활용하면 상속에 유연한 제네릭 코드를 만들 수 있다.

그러나 비한정적 와일드 카드 적용에는 일련의 규칙이 있다. 

 

PECS(펙스)

Producer-Extends Consumer-super 공식이다. 공급할 때는 extends를 활용하고, 사용할 때는 super를 사용하는 공식이다. 

 

Producer - extends (하위 타입은 언제나 상위 타입을 사용할 수 있음)

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

 

위 코드를 보면 s1, s2는 생산자이다. (생산자란 s1,s2를 입력으로 사용해 작업을 한다는 의미이다.)

이 경우 E보다 하위 타입을 입력받더라도 공변이면 E로 자동 형변환해서 저장된다. 

이를 제네릭에서 가능하게 해야하므로,

public static <E> Set<E> union(Set<? extends E> s1, Set<? extends E> s2);

 

위와 같이 작성해야한다.

반환 타입이 E인 이유는 와일드 카드를 통해 리턴시 유연성을 높여주는 효과는 없고, 클라이언트 코드에도 와일드 카드 타입을 써야 하기 때문이다. 

 

Consumer - super (자동 형변환으로 담기)

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

 

이와 같은 코드를 작성한다고 해보자, Collection<E>타입의 dst에 Stack원소들을 담는 메서드이다.

이를 상속에 유연한 공변 입장에서 생각해보면

 

Stack<Number>로 정의된 클래스에 List<Object>타입의 클래스로 Stack에 내용을 뽑고 싶다.

즉, 보다 상위 타입으로 결과를 얻는 것이다. 상위 타입은 언제나 하위 타입을 담을 수 있다.

 

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

 

즉 제네릭을 이와 같이 수정해서 상위 타입에 대해서도 유연하게 인자를 가질 수 있도록 하자


둘 다 사용한 복잡한 경우 

 

public static <E extends Comparable<E>> E max(List<E> list) {}

 

E는 Comparable<E>를 구현한 타입 E이다. 

 

위 경우는 PECS를 모두 적용해야하는 경우이다.

Comparable 인터페이스를 구현한 클래스가 있고, 해당 클래스의 하위 클래스가 있다고 가정하면,

우리가 원하는 결과는 하위 클래스는 따로 Comparable을 구현하지 않더라도 부모 클래스에 구현되어 있기 때문에 부모 클래스에서 정의한 Comparable에 따라 대소관계가 비교되길 원한다. (리스코프 치환)

*여기서 하위 타입은 Comparable이 구현되어 있지 않을 수 있다.! 다만, 해당 타입이 Comparable을 구현했다면, 상위타입도 Comparable을 구현했다고 생각해 볼 수 있다.

 

제네릭에서 Comparable<ParentClass>와 Comparable<ChildClass>는 아무런 관계도 없는 타입이다. 이를 호환되게 만들기 위해서는 Comparable 인자를 E의 상위 타입으로 활용할 수 있어야한다.

 

public static <E extends Comparable<? super E>> E max(List<E> list) {}

 


공급자 소비자 구분

 

공급자: 어떤 인자들을 받아서 사용하는데 상위 타입으로 정의되어 있고, 하위 타입을 받아서 다형성 활용

 

소비자 : 해당 타입이 데이터를 가져가는 경우 (데이터를 가져가는 경우는 해당 타입과 상위 타입만 가능), 상위타입에 정의되어 있고, 하위 타입에 없는 걸 사용할 때 

 


참고자료 

 

https://velog.io/@e1psycongr00/%EC%9D%B4%ED%8E%99%ED%8B%B0%EB%B8%8C-%EC%9E%90%EB%B0%94-item31-%ED%95%9C%EC%A0%95%EC%A0%81-%EC%99%80%EC%9D%BC%EB%93%9C-%EC%B9%B4%EB%93%9C%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%B4-API%EC%9D%98-%EC%9C%A0%EC%97%B0%EC%84%B1%EC%9D%84-%EB%86%92%EC%97%AC%EB%9D%BC

 

이펙티브 자바 #item31 한정적 와일드 카드를 사용해 API의 유연성을 높여라

item 30 에서 잠깐 한정적 와일드 카드를 쓴 경우가 있다. 바로 상속을 고려해서 제네릭을 짜야 할 때이다. 제네릭은 기본적을 불공변이다. List<Object>와 List<String>은 서로 아무런 관계가 없는 타입

velog.io

https://javabom.tistory.com/41

 

[아이템 31] 한정적 와일드카드를 사용해 API 유연성을 높여라

스택 예제 package Chap4_Generic.item31; import java.util.ArrayList; import java.util.List; public class Stack { // 여기의 E와 private List list = new ArrayList(); public void pushAll(Iterable src){ // 여기의 E는 같아야함. 유연하지 못

javabom.tistory.com