#009 제네릭 두번째

제네릭 메소드를 애용하자

// 원천 타입을 사용 - 바람직하지 않다! (항목 23)
public static Set union(Set s1, Set s2) {	// 경고 - Set is a raw type. References to generic type Set<E> should be parameterized
	Set result = new HashSet(s1);	// 경고 - HashSet is a raw type. References to generic type HashSet<E> should be parameterized
	result.addAll(s2);			// 경고 - Type safety: The method addAll(Collection) belongs to the raw type Set. References to generic type Set<E> should be parameterized
	return result;
}

위의 코드는 컴파일은 되지만 경고 메시지가 나온다. 이 경고 메세지를 없애고 메소드가 타입 안전하게 만들려면, 아래와 같이 코딩한다.

import java.util.*; 
 
public class Union {
 
    // 제네릭 메소드
    public static <E> Set<E> union(Set<E> s1, Set<E> s2) {
        Set<E> result = new HashSet<E>(s1);
        result.addAll(s2);
        return result;
    }
 
    // 제네릭 메소드를 사용하는 간단한 프로그램
    public static void main(String[] args) {
        Set<String> guys = new HashSet<String>(
            Arrays.asList("Tom", "Dick", "Harry"));
        Set<String> stooges = new HashSet<String>(
            Arrays.asList("Larry", "Moe", "Curly"));
        Set<String> aflCio = union(guys, stooges);
        System.out.println(aflCio);
    }
}
 
---------- execute ----------
[Moe, Harry, Tom, Curly, Larry, Dick]

union 메소드는 세 Set(메소드 인자 2개와 반환값 1개) 모두의 타입이 같아야 한다는 제약을 갖는데, 이를 더 유연하게 만들고 싶으면 바운드 와일드 카드 타입(bounded wildcard types)을 사용하면 된다.


바운드 와일드 카드를 사용해서 API의 유연성을 높이자
메소드 API에 와일드 카드 타입을 사용하면, 코드 작성이 조금 어려워지긴 하지만 훨씬 유연한 API를 만들 수 있다.
만일 폭넓게 사용할 라이브러리를 작성한다면, 와일드 카드 타입을 올바르게 사용하도록 해야한다.

 import java.util.*;
 
public class Stack<E> {
    private E[] elements;
    private int size = 0;
    private static final int DEFAULT_INITIAL_CAPACITY = 16;
 
    // The elements array will contain only E instances from push(E).
    // This is sufficient to ensure type safety, but the runtime
    // type of the array won't be E[]; it will always be 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; // Eliminate obsolete reference
        return result;
    }
 
    public boolean isEmpty() {
        return size == 0;
    }
 
    private void ensureCapacity() {
        if (elements.length == size)
            elements = Arrays.copyOf(elements, 2 * size + 1);
    }
 
     // Wildcard type for parameter that serves as an E producer
    public void pushAll(Iterable<? extends E> src) {
        for (E e : src)
            push(e);
    }
 
    // Wildcard type for parameter that serves as an E consumer
    public void popAll(Collection<? super E> dst) {
        while (!isEmpty())
            dst.add(pop());
    }
 
    // Little program to exercise our generic Stack
    public static void main(String[] args) {
        Stack<Number> numberStack = new Stack<Number>();
        Iterable<Integer> integers = Arrays.asList(3, 1, 4, 1, 5, 9);
        numberStack.pushAll(integers);
 
        Collection<Object> objects = new ArrayList<Object>();
        numberStack.popAll(objects);
 
        System.out.println(objects);
    }
}

pushAll 메소드에서 와일드 카드를 사용하지 않고 아래 코드와 같이 구현하게 되면,

   public void pushAll(Iterable<E> src) {
        for (E e : src)
            push(e);
    }

Line 68의 numberStack.pushAll(integers)에서 에러가 발생한다.

에러 메세지 – method pushAll(Iterable) in the type Stack is not applicable for the arguments (Iterable)

Stack는 Stack의 서브 타입이 아니기 때문이다.

pushAll의 인자 타입은 “E의 Iterable”이 아닌 “E의 어떤 서브타입의 Iterable”이 되어야 하므로, Iterable 바운드 와일드 카드 타입을 사용한다.
popAll 역시 와일드 카드를 사용하지 않고 아래 코드와 같이 구현하게 되면,

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

Line 71의 numberStack.popAll(objects)에서 에러가 발생한다.

에러 메세지 – The method popAll(Collection) in the type Stack is not applicable for the arguments

Collection Object는 Collection Number의 서브 타입이 아니기 때문이다.
popAll의 인자 타입은 “E 타입을 저장하는 Collection”이 아닌 “E의 어떤 슈퍼 타입을 저장하는 Collection”이 되어야 하므로, Collection 와일드 카드 타입을 사용하면 된다.


타입 안전이 보장되는 혼성(heterogeneous) 컨테이너의 사용을 고려하자
제네릭은 Set이나 Map과 같은 컬렉션과 ThreadLocal이나 AtomicReference 같은 단일 요소(single-element) 저장 컨테이너에 가장 많이 사용된다.
컨테이너의 특성에 따라 사용가능한 타입 매개변수의 개수가 자연스럽게 제한된다.

혼성이란? 컨테이너를 매개변수화 하지 않고 저장되는 요소의 키를 매개변수화 함으로써 서로 다른 타입의 객체들이 같은 컨테이너에 저장 및 검색될 수 있는 것을 혼성 컨테이너라고 부르고 있다.
컨테이너란? 객체를 저장하는 역할을 하는 클래스들을 포괄적으로 컨테이너라고 한다. 컬렉션 클래스들과 배열도 컨테이너이다.
// 타입 매개변수 E - 1개
public interface Set<E> extends Collection<E> {
    ... // 나머지 코드 생략
}
 
// 타입 매개변수 K, V - 2개
public interface Map<K,V> {
    ... // 나머지 코드 생략
}

때로는 더욱 유연한 컨테이너를 필요로 할 때가 있다.
예를 들어, 하나의 데이터베이스 행(row)는 많은 열(column)을 가질 수 있으며, 타입 안전이 보장되는 형태로 모든 열의 데이터 값을 사용할 수 있어서 좋다.
이런 효과를 낼 수 있는 간단한 방법이 컨테이너(container) 대신 키(key)를 매개변수화 하는 것이다.
그 방법에 대한 예제는 아래와 같다.

// 타입 안전이 보장되는 혼성 컨테이너
import java.util.*;
 
public class Favorites {
    //  타입 안전이 보장되는 혼성 컨테이너 패턴 - implementation
    private Map<Class<?>, Object> favorites =  // 주목할 점! - favorites Map의 값의 타입이 Object므로, 키와 값 간의 타입 관계가 유지되도록 보장하지 않는다
        new HashMap<Class<?>, Object>();
 
    public <T> void putFavorite(Class<T> type, T instance) {
        if (type == null)
            throw new NullPointerException("Type is null");
        favorites.put(type, instance);
    }
 
    public <T> T getFavorite(Class<T> type) {
        return type.cast(favorites.get(type));
    }
 
    //  타입 안전이 보장되는 혼성 컨테이너 패턴 - client
    public static void main(String[] args) {
        Favorites f = new Favorites();
        f.putFavorite(String.class, "Java");
        f.putFavorite(Integer.class, 0xcafebabe);
        f.putFavorite(Class.class, Favorites.class);
 
        String favoriteString = f.getFavorite(String.class);
        int favoriteInteger = f.getFavorite(Integer.class);
        Class<?> favoriteClass = f.getFavorite(Class.class);
        System.out.printf("%s %x %s%n", favoriteString,
                          favoriteInteger, favoriteClass.getName());
    }
}
 
---------- execute ----------
Java cafebabe Favorites

Favorites 인스턴스는 타입 안정이 보장된다(String을 요청하면 절대로 Integer를 반환하지 않는다).
Favorites 인스턴스는 혼성이다(일반적인 Map과 다르게 모든 키가 서로 다른 타입일 수 있다).
따라서, Favorites를 타입 안전 혼성 컨테이너(typesafe heterogeneous container)라고 한다.

creative by javacafe

댓글 남기기

이메일은 공개되지 않습니다. 필수 입력창은 * 로 표시되어 있습니다

%d 블로거가 이것을 좋아합니다: