More fun with generics

2004년 6월에 쓴 글을 수정함;;

———————————

Legacy 클래스와 동일한 signature를 갖는 Generics만들어서 max 함수 구현하기. 튜토리얼에서 보고 만들어봄.

아마 이정도가 자바 generics의 가장 어려운 수준인 거 같습니다. (지금까지 본 문서에 의하면..)

그런데, 사실 너무 어렵 -_-;

코드는 확실히 간단해지는데 코드의 복잡도가 몽땅 메소드 시그니처로 가버리는군요.. 사실 이 정도로 이런 저런거 다 생각하면서 짜다간 홧병날지도..

import java.util.*;

public class LegacyToGenerics {

    public static void main(String[] args) {

        ArrayList intList = new ArrayList();
        intList.add(new Integer(1));
        intList.add(new Integer(3));
        intList.add(new Integer(2));
        intList.add(new Integer(5));

        ArrayList<Integer> intList2 = new ArrayList<Integer>();
        intList2.add(1);
        intList2.add(3);
        intList2.add(2);
        intList2.add(5);

        Legacy l = new Legacy();
        Generics g = new Generics();

        System.out.println(l.max(intList));
        System.out.println(g.max(intList2));

        System.out.println();
    }
}

class Legacy {

    public Object max(Collection coll) throws ClassCastException {

        if (coll.size() == 0)
            throw new NoSuchElementException("List is empty");

        Iterator i = coll.iterator();
        Object max = null;

        while (i.hasNext()) {

            if (max == null) max = i.next();
            else {
                Comparable c = (Comparable) max;
                Object other = i.next();
                if (c.compareTo(other) < 0) max = other;
            }
        }

        return max;
    }
}

class Generics {

    // Object&Comparable is required to ensure that
    // this method will return Object
    public <T extends Object&Comparable<? super T>>
    T max(Collection<? extends T> coll) {

        if (coll.size() == 0)
            throw new NoSuchElementException("List is empty");

        T max = null;

        for(T other: coll) {

            if (max == null) max = other;
            else {
                if (max.compareTo(other) < 0) max = other;
            }
        }

        return max;
    }
}

어려운 부분이 max 함수의 선언부죠.
public <T extends Object&Comparable<? super T>>
T max(Collection<? extends T> coll)

일단 T라는 원소들의 배열이 있고, 우리는 여기서 최대값을 찾고자하고 있습니다. 그리고 실제 최대값을 구하는 과정을 보면 for loop을 돌면서 원소들을 compareTo로 비교합니다. 따라서 우리는 T가 comparable을 구현하고 있기를 원합니다. 결과적으로

T extends Comparable<T>

가 성립하죠. 그런데, 곰곰히 생각해보면 T는 반드시 T 자신만을 비교하는 녀석을 상속할 필요가 없습니다. 그보다는 T또는 T의 부모를 상호 비교할 수 있으면 okay입니다.

이해를 돕기 위해, 예를 들어 위해 T extends S라고 하겠습니다. 그리고, S extends Comparable<S> 라고 합시다. 이 경우, 자동적으로 T extends Comparable<S>가 됩니다. 따라서 T 끼리는 비교가 가능한 상태가 되고, 이는 우리의 max구현에서는 유효한 클래스 계층구조가 됩니다.

따라서 앞서의 T extends Comparable<T>는

T extends Comparable<? super T>

가 됩니다. 여기서 ? super T라는건,T의 부모인 ‘누군가’를 의미합니다. 그리고 java generics에서는 이처럼 ?를 사용해 명시적인 이름을 주지 않을 수도 있습니다.

그리고 다시 메소드의 시작 부분을 보면

public <T extends Object&Comparable<? super T>> T max(…)

라고 되어있습니다. 여기서 Object& 라는 부분은 erasure로서, max의 반환값 T에 사용하는 자료형태입니다. (네.. java generics는 C++ 의 template과는 다릅니다.) 앞에서 T라는 것에 대해 우리는 Comparable<? super T>를 상속하고 있으면 okay 라고 했습니다. 따라서 max의 반환값은 T1이라는 클래스가 될 수도 있고, T2라는 클래스가 될 수도 있고 여러가지가 될 수 있겠죠. 하지만 우리는 legacy와 동일한 코드를 구현하고자 하고 있으며 legacy에서는 object만을 반환하게 되어있습니다. 따라서 우리도 object로 반환하게 선언을 해야하고 따라서 이러한 erasure를 씁니다.

다음은 인자인 Collection<? extends T> coll 을 봅시다. 우리는 T에 대한 collection에서 max를 구하고자 합니다. 따라서 인자는 Collection<T> 이면 될것입니다. 그러나 객체지향에 대한 대강의 감이 잡힌사람은 아시겠지만, 컬렉션이 T에 대한 타입이면 당연히 그 컬렉션안에는 T의 서브클래스들이 들어있을 수 있습니다. 따라서 Collection<? extends T>라고 선언하여 T또는 T를 상속한 서브클래스에 대한 컬렉션을 인자로 받게 하여 일관성을 확보합니다.

결과적으로, public <T extends Object&Comparable<? super T>> T max(Collection<? extends T> coll) 와 같은 메소드 시그니처가 나오게 되는 것이죠.

다시 근본적인 설명으로 돌아가서.. 앞서 *erasure*라는 단어를 사용했습니다. erasure란 말뜻대로 지우개입니다. C++의 경우엔 템플릿을 정의하고, 템플릿에 여러 타입을 적용한 경우 다수의 코드가 생성됩니다(code specialization). 반면, 자바의 경우엔 generic class라고 할지라도 하나의 byte code만 존재합니다(code sharing). 또, 이 generic class의 type T에 어떤 인자가 오던지간에 이 byte code에대한 instance로서 존재합니다. 이를 위해, List<Integer> 나 List<String>은 둘다 List라는 generic이 지원되기 이전형태의 클래스로 변환되야 합니다. 이 과정에서 <Integer>, <String>라는 부분이 삭제가 되게 됩니다. 또 이런 삭제 과정에서는 당연히 타입명뿐만 아니라, 메소드에 대한 인자등도 모두 삭제됩니다.

그러나 삭제를 할땐 하더라도 예전의 자바 소스와는 같은 형태의 소스를 사용해주어야합니다. 즉, 우리는

public <T extends Object&Comparable…> T method_name(..)
{
}

라는 메소드에서 <T…> T 부분을 *erase*할때 Object라는 erasure를 사용하여

public Object method_name (…)
{
}

와 같이 변환하게됩니다. 이것이 Object&Comparable.. 에서 Object& 부분이 erasure라고 불리는 이유입니다. 정말로 지우니까요. 자바의 generics란 근본적으로 이와같이 code sharing and erasure 로 구성되어 있습니다.

Similar Posts:

Post a Comment

Your email is never published nor shared.