2016년 3월 28일 월요일

Java Generic의 모든 것

의미와 기본 용법

  • Java Generic Type은 클래스의 타입을 parameter로 만든 것이다. 
  • Java 5 (SDK 1.5)부터 추가되었으며 주로 Java Collection에서 많이 사용된다.
  • 다루는 클래스의 타입은 일반적으로 선언하면서도 실제 사용할 때에는 구체적으로 어떤 클래스 타입인지 명시하게 함으로써 내부적으로, 또는 사용시 형변환을 하지 않아도 되게 한다. (컴파일러가 자동으로 캐스팅 해준다)
  • 만일 일반적인 클래스를 Object로 선언하면 사용할 때 구체 클래스 타입으로 형변환을 해야하는데, 이때 실수를 하게되면 run time에 가서야 detect를 할 수 있다. 

public class Box {
    private Object object;

    public void set(Object object) {
        this.object = object;
    }

    public Object get() {
        return object;
    }
}



  • 예를 들어, 위의 클래스를 사용하는 코드를 보자. 

...
    Box box = new Box();
    box.set(new String("Hello World"));
    // force downcasting regardless of type in box
    String inbox = (String)box.get();
...



  • Box안에 어떠한 클래스가 들어 있든지 위의 코드는 꺼내서 String타입으로 강제 형변환을 시킨다. 이 부분이 잘못되어도 compile time에는 알 수 없다.
  • 이것을 generic type으로 선언해보자. 

public class Box<T> {
    // T stands for "Type"
    private T t;

    public void set(T t) {
        this.t = t;
    }

    public T get() {
        return t;
    }
}



  • 그러면 아래와 같이 사용할 수 있다. (아래의 type은 String Box라고 읽는다)

...
    Box box<String> = new Box<String>();
    box.set(new String("Hello World"));
    // downcasting is not needed
    String inbox = box.get();
...



  • Box안에 String외의 다른 클래스를 넣으려는 시도는 compile time에 detect된다. 그리고 꺼내 쓸 때에는 따로 downcasting이 필요 없다. 
  • 두 개 이상의 복수개 type을 사용할 수도 있다. 

public class Box<T, S> {
    private T first;
    private S second;

    public (T t, S s) {
        this.t = t;
        this.s = s;
    }

    public T getFirst() { return t; }
    public S getSecond() { return s; }
}



Type parameter의 naming convention

  • E - Element (used extensively by the Java Collections Framework)
  • K - Key
  • N - Number
  • T - Type
  • V - Value
  • S, U, V etc. - 2nd, 3rd, 4th types


Generic Method와 type inference

  • 꼭 클래스 단위의 사용이 아니라도 좋다. 메소드에서 사용할 수 있는데, 그것을 Generic method라 부른다. 메소드 시그니처를 유념하라. (U는 클래스 선언부에 사용되지 않은 type parameter이다)

public class Box<T> {
...
    public <U> void printBox<U info> {
        System.out.println(info);
    }
...



  • 사용하는 코드를 보자.

...
    Box box<Integer> = new Box<Integer>();
    box.set(new Interger(1));
    // downcasting is not needed
    box.printBox("Hello World");
...



  • 함수 call에 대해서 명시적으로 어떤 타입인지 선언하지 않았는데도 잘 돈다. 이는 컴파일러가 printBox의 인자가 String타입임을 알고 있기 때문에 U가 String인 것도 알기 때문이다. 이러한 처리를 타입추론 (type inference)라고 한다. 
  • 뿐만 아니라 타입추론를 통해 생성자에서 타입 생략도 가능하다. 예를 들어 아래와 같은 Generic type 클래스의 생성자가 있을 때

public class Box<T, S> {
    private T first;
    private S second;

    public (T t, S s) {
        this.t = t;
        this.s = s;
    }
...



  • 아래와 같은 코드도 동작한다. 

    Box box<Integer, String> = new Box<Integer, String>(new Integer(1), "Hello");
    Box box = new Box(new Integer(1), "Hello");



  • 위의 두 줄은 동일하게 동작한다.


Type Bound

  • 상속 관계를 선언함으로써 사용되는 type에 제한을 가할 수도 있다 
  • 한정적 형인자 (bounded type parameter)

public class Box<T extends Numbers> {
...


    • 여기서 설령 interface를 상속 받는다고 하더라도 'implements' 키워드를 사용하지 않고 'extends' 키워드를 공통적으로 사용한다. 
  • 재귀적 형 한정 (recursive type bound)

public class Box<T extends Comparable<T>> {
...


    • 상속받는 타입이 하필 generic type일 경우 이런 일이 벌어진다.



Wildcard

  • Generic type class을 parameter로 사용할 때 아직 구체 타입을 정하지 않은 경우라면 wildcard를 쓰는 것이 좋다.
  • 비한정적 와일드카드 자료형(unbounded wildcard type)

static int numElementsInCommon(Set<?> s1, Set<?> s2) {
    int result = 0;
    for (Object o1: s1)
        if (s2.contains(o1))
            result++;
    return result;
}



    • 즉, 어떤 class type에 대한 Set인지 상관없이 Set의 메소드를 이용하고 싶을 때 위와 같은 코드가 된다. 참고로 위의 예제에서 Set의 두 타입은 서로 달라도 무방하다.
    • 그렇다면 위의 코드를 그냥 원천 타입 (raw type)의 Set으로 사용하면 어떨까?

static int numElementsInCommon(Set s1, Set s2) {
    int result = 0;
    for (Object o1: s1)
        if (s2.contains(o1))
            result++;
    return result;
}



    • 메소드 시그니처 빼고는 다를게 없다. 그러나 이렇게 사용하지 마라. 왜냐하면 Set의 원천타입을 사용한다는 것은 Set이 타입에 대해 안전하지 않다는 뜻이다.
  • 한정적 와일드카드 자료형(bounded wildcard type)
    • 파라메터 클래스 타입을 제한하고 싶다면 와일드카드와 extends 키워드를 사용하면 된다.

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

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



    • 위의 코드에서 pushAll()과 popAll()은 잘 돌까?
    • 아래와 같이 pushAll()을 사용할 경우 컴파일 에러가 발생한다.

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



    • 왜냐하면 Integer는 Number이지만 Stack<Integer>와 Stack<Number>는 아무런 상관이 없기 때문이다.
    • popAll()의 경우도 마찬가지이다.

    Stack<Number> numberStack = new Stack<Number>();
    Collection<Object> objects = ...;
    numberStack.popAll(objects);



    • 따라서 두 메소드의 선언부는 아래와 같이 수정되어야 한다.

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

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



    • super 한정자는 어떤 클래스의 조상 클래스여야 한다는 제한이다.
    • 보통 in-param에서는 extends를 사용하고 out-param에서는 super을 사용하고, in-out param에서는 wildcard를 사용하지 않는 것이 좋다. (특히 out-param에서 extends를 사용하는 실수를 하지 마라)


Generic type의 생략

  • Java 7부터는 generic type의 객체를 생성할 때 선언부에 타입 변수를 적었다면 instance 생성부에서는 타입 변수를 생략할 수도 있다. (꺽쇄만 남는다)

    Box box<Integer> = new Box<>();


Java Generic type vs. C++ Template

  • Java Generic typ과 C++ Template는 생긴 것은 비슷하지만 두 언어가 이를 처리하는 과정은 아주 많이 다르다.
  • Java의 Generic은 타입 제거 (type erasure)라는 개념에 근거한다. 이 기법은 소스 코드를 Java 가상 머신 (JVM)이 인식하는 바이트 코드로 변환할 때 인자로 주어진 타입을 제거하는 기술이다.
  • 가령 아래와 같은 코드를 작성했다면

ArrayList<String> list = new ArrayList<String>();
list.add(new String("hello"));
String str = list.get(0);


  • 컴파일러는 이 코드를 다음과 같이 변환한다. 즉, Generic code들은 컴파일 타임에 모두 제거되고 컴파일러가 자동으로 downcasting을 해주는 것이다.

ArrayList list = new ArrayList();
list.add(new String("hello"));
String str = (String) list.get(0);


  • 따라서 Java Generic이 있다고 해서 크게 달라지는 것은 없다. 뭔가를 좀 더 예쁘게 작성할 수 있게 되었을 뿐이다. 그래서 Java Generic을 때로 '문법적 양념 (syntactic sugar)'라고 부르는 것이다.
  • C++의 경우에는 상황이 좀 다르다. C++에서 Template은 좀 더 우아한 형태의 매크로다. 컴파일러는 인자로 주어진 각각의 타입에 대해 별도의 템플릿 코드를 생성한다. MyClass<Foo>가 MyClass<Bar>와 static 변수를 공유하지 않는 것을 보면 알 수 있다. (MyClass<Foo>로 만든 두 객체는 static 변수를 공유한다)
  • 반면 Java static 변수는 MyClass로 만든 모든 객체가 공유한다. 제네릭 인자로 어떤 타입을 주었는지 관계 없다.
  • 이러한 구조적 차이 때문에 Java Generic과 C++ Template에는 다른 점이 많다. 그 중 일부는 다음과 같다.
    • C++ Template에는 int와 같은 기본 타입을 인자로 넘길 수 있다. Java Generic에서는 불가능하다. 모든 타입은 Object를 상속해야 하며 따라서 int 대신 Integer를 사용해야 한다.
    • Java의 경우, 제네릭 타입 인자를 특정한 타입이 되도록 제한할 수 있다. 가령 CardDeck을 제네릭 클래스로 정의할 때, 그 인자로는 CardGame의 하위 클래스만 사용되도록 제한하는 것이 가능하다. (한정적 형인자)
    • C++ Template은 인자로 주어진 타입으로부터 객체를 만들어 낼 수 있다. Java에서는 불가능하다.
    • Java에서 Generic type 인자는 static 메서드나 변수를 선언하는데 사용될 수 없다. MyClass<Foo>와 MyClass<Bar>가 공히 이 메서드와 변수를 공유할 것이기 때문이다. C++에서는 이 두 클래스는 다른 클래스이므로 템플릿 타입 인자를 static 메서드나 변수를 선언하는데 사용할 수 있다.
    • Java에서 MyClass로 만든 모든 객체는 제네릭 타입 인자가 무엇이냐에 관계없이 전부 동등한 타입이다. 실행시간에 타입 인자 정보는 삭제된다. C++에서는 다른 템플릿 타입 인자를 사용해 만든 객체는 서로 다른 타입의 객체이다.




Android에서 Parcelable한 generic class 만들기

  • 핵심은 class loader인데 generic type (T 변수)의 getClass().getClassLoader()를 이용한다.

public class MyParcelableClass<T> implements Parcelable {
    private T mValue;

    @Override
    public void writeToParcel(Parcel parcelOut, int flags) {
        parcelOut.writeValue(mValue);
    }

    @SuppressWarnings("rawtypes")
    public static final Parcelable.Creator<GenericParcelable> CREATOR = new Parcelable.Creator<GenericParcelable>() {
        ...
    }

    public MyParcelableClass(T value) {
        this.mValue = value;
    }

    @SuppressWarnings("unchecked")
    private GenericParcelable(Parcel parcelIn) {
       try {
            mValue = (T)parcelIn.readValue(mValue.getClass().getClassLoader());
        } catch(Exception e) {
            e.printStackTrace();
        }
    }

    public T getValue() {
         return (T) mValue;
    }
}



  • 그런데 generic class를 AIDL에서 선언할 수 없는데 그렇다고 <>를 빼고 선언하면 코드에서 warning이 발생한다. 역시 현재로선 @SuppressWarnings("unchecked")를 사용하여 warning을 무시하는 수 밖에 없다.

댓글 1개:

  1. 훌륭한 정리 잘 읽었습니다.
    혹시 책을 읽고 정리하신 거라면 책 제목을 알 수 있을까요?

    답글삭제