- 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을 무시하는 수 밖에 없다.
훌륭한 정리 잘 읽었습니다.
답글삭제혹시 책을 읽고 정리하신 거라면 책 제목을 알 수 있을까요?