Notice
Recent Posts
Recent Comments
Link
«   2025/05   »
1 2 3
4 5 6 7 8 9 10
11 12 13 14 15 16 17
18 19 20 21 22 23 24
25 26 27 28 29 30 31
Archives
Today
Total
관리 메뉴

전공공부

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

Study/Java

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

monitor 2023. 4. 4. 21:12

우선 장을 시작하기 전 공변과 불공변의 개념을 집고 넘어가는 것이 좋다.

 

공변과 불공변의 개념

공변과 불공변은 각각 다음과 같다.

  • 공변(covariant) : A가 B의 하위 타입일 때, T <A> 가 T<B>의 하위 타입이면 T는 공변
  • 불공변(invariant) : A가 B의 하위 타입일 때, T <A> 가 T<B>의 하위 타입이 아니면 T는 불공변

 

대표적으로 배열은 공변이며, 제네릭은 불공변인데 이를 코드로 살펴보도록 하자. 예를 들어 배열의 요소들을 출력하는 메소드가 있다고 하자. 이때 우리가 변수의 선언 타입은 Integer로, 메소드의 선언 타입은 Object로 해두었다고 하자.

void Test() {
    Integer[] integers = new Long[]{1, 2, 3};//(공변) 컴파일은 되지만 런타임에러
    ArrayList<Object> list = new ArrayList<String>(); 
    //(불공변)명시화를 하므로 
    //공변상태가 될 수 없을 뿐더러 애초에 Object List는 아무 객체나 받을 수 있으나,
    // String List는 문자열만 받을 수 있으나 치환 법칙에 어긋난다.
}

 

매개변수화 타입은 불공변이다. 그러나, 더 유연한 방식의 무언가가 필요하다.

 

생산자 매개변수에 와일드 카드 타입 적용

public class Stack<E>{
	public Stack();
    public void push(E e);
    public E pop();
    public boolean isEmpty();
    
    public pushAll(Iterable <E> src){
    	for(E e : src)
        	push(e);
    }
}

 

Stack<Number> s = new Stack<>();

Iterable<Integer> integers = ...;

s.pushAll(integers);

참고로, Integer는 Number의 하위 타입이다.

 

당연히, 위 명령어는 제대로 동작하지 않는다. 매개변수화 타입에서는 불공변이기 때문이다.

 

Number 타입의 pushAll 메서드는 Integer 타입에서 사용 할 수 없다.

 

하지만,

public class Stack<E>{
	public Stack();
    public void push(E e);
    public E pop();
    public boolean isEmpty();
    
    public pushAll(Iterable <? extends E> src){
    	for(E e : src)
        	push(e);
    }
}

 

생성자 매개변수로 <? extends E>를 사용하는 이유는 Integer로 들어온 객체가 Number는 만들 수 있으나 하위에서 상위 객체 만들기 가능

 

(<? super E>가 되지 않는 이유는 Object 객체가 Stack<Number>에 인스턴스로 쌓일 수 없다 )

 

 

Stack<Number> s = new Stack<>();
Iterable<Integer> integers = ...;
s.pushAll(integers);

해당 코드는 정상 동작 할 수  있다.

 

 

소비자 매개변수 (나의 객체를 사용하여 구성한 것) 에 와일드 카드 타입 적용

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

}

이번에는 객체를 지우는 행위를 하기 위해서 사용하는 메서드이다.

 

Stack<Number> s = new Stack<>();
Collections<Object> objects = ...; //클론 떠서 형변환 안 했을 경우
s.popAll(objects);

 

이는 다시 오류를 발생 시킨다.

 

왜냐하면 Object는 Number으로 치환 될 수 없기 때문입니다. (제네릭은 불공변)

 

고로, 이를 다시 아래와 같이 수정하면 작동을 할 수 있는데 이는 간단히 소비자 매개변수 E 객체는 현재 나의 객체로 메서드의 행동 양식을 구성하여서 만일 상위 객체가 아닌 하위객체가 해당 메서드를 사용하려고 접근하면 필수적으로 필요한 것들이 누락되어 있을 수 있으므로 소비자 매개변수는 항상 <? super E>를 사용하게 된다.

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

}

 상위 객체가 하위 객체를 받을 수 있으니 가능

 

( <? extends E > -> Integer 타입이 Number 타입을 가져가서 사용을 한다고하면 필수 변수 값이 누락이 되었을때 동작 하지 않을 것이다.)

 

왜 생성하는 메서드이면 <? extends E>를 쓰고 삭제하면 <? super T>를 쓸까?

왜냐하면 생성시에는 상위 객체를 이용해서 생성을 될 수 없는 이유가 하위 객체 필수적인 요소들이 갖춰지지 않은 상위 객체이면 생성을 할 수 없다. (Object가 Number를 만드는 경우) 그래서, <? extends E>를 사용한다.

 

반대로, 소비할때는 하위 객체로 소비 할 수가 없는 것이 하위 객체의 필수적인 부분이 누락된 E 객체라면 메서드 내부에서 잘 동작 하지 않을 수 있다.

 

(소비의 예시로는 Comparable<String>으로 만들어진 것이 Object의 값을 비교 할 수는 없는 노릇 -> String을 구성하기 위한 필요 객체들이 Object에 구현되지 않았으니 타입 매칭 자체가 불가)

 

조금 더 깊게 들어가 보자...

public class Union {
//s1,s2 모두 생산자 매개 변수이니 extends를 사용하였다.
    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;
    }

    public static void main(String[] args) {
        // Set.of 메서드는 java 9 이상부터 지원
        Set<Double> doubles = Set.of(1.0, 2.1);
        Set<Integer> integers = Set.of(1, 2);
        Set<Number> unionSet = union(doubles, integers);
        //자바 7에서는 아래와 같이 설정해야한다.
        Set<Number> unionSet7 = Union.<Number>union(integers,doubles)
    }
}

위 코드에서 s1, s2는 각각 생성자 매개변수로 extends를 사용하였다.

 

그리고, 반환 타입에서는 한정적 와일드카드를 쓰지 않았다. (<? extends E> 같은 것) 유연성을 높이기 위해서는 클라이언트 단에서 한정된 와일드 카드 타입을 쓰지 않게 하는 것이 좋다고 판단되기 때문이다.

 

그리고, 위 주석을 참고하면 자바7에서는 유니온 메서드를 명시적 타입 인수를 사용하지 않는다면 오류가 나게 되는데 이는 자바8 부터 목표 타이핑을 지원하였기 때문이라고한다.

 


이번에는 max 메서드를 어떻게 쓸지 보자

 

// 변경 전
public static <E extends Comparable<E>> E max(Collection<E> collection)

// 변경 후(PECS 공식 2번 적용)
public static <E extends Comparable<? super E>> E max(Collection<? extends E> collection)

왜 이것을 Comparable <? super E>로 변경하였는가를 생각해보면 Comparable은 E 인스턴스를 소비하게 되기 때문이다. (반대로 E의 인스턴스를 생성하지 않는다.)

 

max 메서드는 결국 객체 인스턴스를 생성하여 반환하기 때문에 인스턴스를 생성한다 그래서 <? extends E> 가 사용되는 것이 맞다.

 

그래서 위와 같이 수정하여 사용하였는데... 이를 실제로 사용 할 수 있는 예제가 있을까?

 

답은 그렇다이다. Comparable을 직접 구현하지 않은 구현체에서 max 메서드를 사용하려면 위와 같이 수정을 해야한다.

 

실제로 사용 될 수 있는 객체를 보자.

 

 


해당 객체는 Delayed 객체를 상속 받으며 내부적으로 Comparable을 직접 구현하진 않았다.

 

public interface ScheduledFuture<V> extends Delayed, Future<V> {
}//해당 객체는 Delayed 객체를 상속 받으며 Comparable을 구현하지 않았다.
public interface Delayed extends Comparable<Delayed> {
//Delayed 객체는 Comparable을 구현하였다.
    /**
     * Returns the remaining delay associated with this object, in the
     * given time unit.
     *
     * @param unit the time unit
     * @return the remaining delay; zero or negative values indicate
     * that the delay has already elapsed
     */
    long getDelay(TimeUnit unit);
}

 

 

 

    public static <E extends Comparable<E>> E max(Collection<E> collection)

위의 예제는 작동하지 않는다. 

 

애초에 매개변수화 타입은 불공변, 위 케이스가 가능하려면 해당 ScheduledFuture 객체가 배열처럼 Delayed 객체를 수용 할 수 있어야한다. (List<ScheduledFuture<?>> list  해당 리스트에 Delayed 객체가 저장 될 수 있어야 함)

 

 

아래의 예제는 작동할 것이다.

 

    	//max 메서드는 객체를 생성함 그러므로 고로, <? extends E>
        //Comparable의 E 인스턴스는 내부 객체에서 생성된 E 인스턴스를 소비함 고로, <? super E>
        //ScheduledFuture는 Delayed에서 비교할 수 있는 모든 속성들을 다 가지고 있다.
        public static <E extends Comparable<? super E>> E max(Collection<? extends E> collection)

E는 ScheduledFuture로 생성되었고,이 E는 Comparable을 직접 구현하지 않았다.

 

ScheduledFuture는 Delayed의 하위 인터페이스이고 Delayed는 Comparable을 직접 구현하였다. 그래서 E의 상위 객체인 Comparable을 구현한 Delayed의 Comparable을 사용하여 비교 연산을 진행 할 수 있을 것이다.

 

 

 

 

 

와일드 카드 사용의 주의점

public static <E> void swap(List<E> list, int i, int j);

public static void swap(List<?> list, int i, int j);
public API라면 간단한 두 번째가 낫다. 첫번째 경우는 리스트 타입을 무조건 명시를 해줘야 넘길 수 있을 것이다.
 
하지만 아래 코드는 컴파일하면 그다지 도움이 되지 않는 오류 메시지가 나온다고 한다.
public static void swap(List<?> list, int i, int j) {
	list.set(i, list.set(j, list.get(i));
}
 
원인은 리스트의 타입이 List<?>인데, List<?>에는 null 외에는 어떤 값도 넣을 수 없어서 그렇다고 한다.
 
다행히 런타임 오류를 낼 가능성이 있는 형변환이나 리스트의 로 타입을 사용하지 않고도 해결할 방법이 있다.
 
바로 와일드카드 타입의 실제 타입을 알려주는 메서드를 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.set(i, list.set(j, list.get(i));
}
 

 

이렇게 하면 swap을 구현한 객체로 부터 상위 객체의 swap을 자유롭게 할 수 있을 것이다.