Java Generics 03 : Generic Method

from : https://docs.oracle.com/javase/tutorial/extra/generics/methods.html

Generic Methods 
다음은 배열객체를 받아서 컬렉션 객체에 추가하는 예를 보여준다.
static void fromArrayToCollection(Object[] a, Collection<?> c) {
    for (Object o : a) {
        c.add(o);  // compile-time error
    }
}
이 코드는 Collection<Object>에서 컬렉션 파라미터를 쓰는데 있어 초보자가 저지르는 실수 이다.  Collection<?> 을 이용하며 이 컬렉션에서 어떠한 객체의 컬렉션을 쓰는지 인식하지 못한다.
알려지지 않은 타입의 컬렉션에서는 객체들을 받아 들이지 않는다.

이러한 문제를 해결하기 위해서는 generic methods를 이용하면 된다.
타입 선언과 같이 메소드 선언도 generics로 할수 있다. 이것은 파라미터로 하나 혹은 몇개의 파라미터 타입을 받을 수 있다.

static<T> void fromArrayToCollection(T[] a, Collection<T> c) {
    for (T o : c) {
        c.add(o); //Correct
    }
}
이 메소드는 어떠한 타입의 메소드든지 이용가능하다. 그리고 엘리먼트 타입은 배열의 서브타입으로 어떠한 타입이 와도 상관이 없다.

Object[] oa = new Object[100];
Collection<Object> co = new ArrayList<Object>();
// T 가 Object로 판단한다.
fromArrayToCollection(oa, co);

String[] sa = new String[100];
Collection<String> cs = new ArrayList<String>();
// T가 String이라고 판단한다.
fromArrayToCollection(sa, cs);

// T가 Object라고 판단한다.
fromArrayToCollection(sa, co);

Integer[] ia = new Integer[100];
Float[] fa = new Float[100];
Number[] na = new Number[100];
Collection<Number> cn = new ArrayList<Number>();

// T가 Number이라고 판단한다.
fromArrayToCollection(ia, cn);

// T가 Number이라고 판단한다.
fromArrayToCollection(fa, cn);

// T가 Number이라고 판단한다.
fromArrayToCollection(na, cn);

// T가 Object라고 판단한다.
fromArrayToCollection(na, co);

// 컴파일 에러가 발생한다.
fromArrayToCollection(na, cs);

Generic메소드에 실제 타입 아규먼트가 전달될 필요는 없다. 컴파일러는 타입 아규먼트를 스스로 추론한다. 이것은 실제 아규먼트의 타입에 기반한 추론을 한다.
이때 일반적으로 가장 구체적인 타입 아규먼트를 선택한다.

Generic method에 대한 질문 :
1. 언제 generic method를 사용하는가?
2. 언제 wildcard타입들을 사용하는가?

이 질문에 답을 이해하기 위해서 다음 메소드를 살펴보자.

interface Collectioin<E> {
    public boolean containsAll(Collection<?> c);
    public boolean addAll(Collection<? extends E> c);
}
우리는 이 코드를 generic메소드를 이용할 수 있다.
interface Collection<E> {
    public <T> boolean containsAll(Collection<T> c);
    public <T extends E> boolean addAll(Collection<T> c);
    // 타입 변수는 bounds를 가질 수 있다. (bounds는 이전 내용 참조....)
}
여기서는 containsAll과 addAll 둘다 오직 parameter type으로 T를 한번씩만 사용했다. 반환 비타은 타입 파라미터에 의존하지 않는다.
이것은 타입아규먼트는 polymorphism이 된다는 것을 의미한다. 서로다른 호출 환경에서 다양한 실제 파라미터를 수용할 수 있다는 의미이다.
이때 wildcard를 그중 하나로 사용할 수 있다. Wildcard는 flectible subtyping을 지원해주도록 설계 되었다.

Generic method는 타입파라미터가 메소드로 들어가는 파라미터 혹은 반환 타입들 중 하나에 의존되도록 해준다. 만약 의존성이 없다면 generic method는 이용할 수 없다.

다음은 generic methods와 wildcard를 둘다 사용할 수 있다. Collections.copy()가 그 예이다.
class Collections {
    public static<T> void copy(List<T> desc, List<? extends T> src) {
        ...
    }
}
두 파라미터 타입의 의존관계를 보자. src로 부터 객체를 복사하고, dest리스트에 엘리먼트 타입 T에 해당하는 곳에 할당 될 수 있어야 한다. 그래서 위 코드는 T의 서브타입으로 src가 올 수 있다. 즉, copy 시그너쳐는 타입 파라미터를 이용하여 의존성을 표현한다. 그러나 두번째 타입에서 wildcard를 이용한 예를 보자.

우리는 이 메소드의 시그너쳐를 다음과 같이 작성할 수 있다.
class Collections {
    public static<T, S extends T> void copy(List<T> desc, List<S> src) {
        ...
    }
}
이것은 좋다. 그러나 첫번째 파라미터는 dest의 타입에서 그리고 두번째 파라미터의 바운드에서 사용되어 진다. S는 자체적으로 src타입에서 한번만 사용된다. 의존성이 없다. 이것은 S를 와일드카드로 바꿀수 있다는 의미이다.
wildcard를 이용하는 것은 타입 파라미터를 선언하는 것에 비해서 간결하고 명확하다. 그리고 어디에서든지 사용이 가능하다.

wildcard는 메소드 시그너쳐 밖에서도 사용할 수 있는 이점이 있다. 타입의 필드나 지역변수 그리고 배열 등에서 사용이 가능하다.

shape drawing문제로 돌아가보자. 우리는 drawing 요청에 대한 이력을 저장하기를 원한다고 가정하자. 이때 Shape 클래스의 내부에 정적 변수에 이력을 저장하고 관리할 수 있다. 그리고 drawAll()은 들어오는 아규먼트를 history필드에 저장할 수 있다.

static List<List<? extends Shape>> history = new ArrayList<List<? extends Shape>>();
public void drawAll(List<? extends Shape> shapes) {
    history.addLast(shapes);
    for (Shape s : shapes) {
        s.draw(this);
    }
}
마지막으로 타입 파라미터의 이름 규칙에 대해서 확인해보자. T를 타입의 이름으로 사용했다. 그러나 이것은 타입을 구분할 수 있는 정보를 제공하지 않는다.
generic methods에서 자주 발생하는 케이스이다.

만약 복수개의 타입 파라미터를 사용하는경우 T와 근접한 S를 이용할 것이다.
만약 generic 메소드가 generic 클래스내에 만들어 지면 혼란을 피하기 위해서 동일한 이름을 사용하는 것을 피하는 것이 좋다.

이것은 중첩된 generic클래스에서도 동일하게 적용된다.






Share this

Related Posts

Previous
Next Post »