제네릭은 Set<E>
, Map<K,V>
등의 컬렉션과 ThreadLocal<T>
, AtomicReference<T>
등의 단일 원소 컨테이너에도 흔히 쓰인다. 하지만 이처럼 클래스 레벨에서 매개변수화 할 수 있는 타입의 수는 제한적이다. (e.g Map 2개, Set 1개)
public class Favorite {
private Map<Class<?>, Object> favorites = new HashMap<>();
public <T> void putFavorite(Class<T> type, T instance) {
favorites.put(Objects.requireNonNull(type), type.cast(instance));
}
public <T> T getFavorite(Class<T> type) {
return type.cast(favorites.get(type));
}
}
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());
}
위 코드에서 각 타입의 Class 객체를 매개변수화한 키 역할로 사용하고 있는데 이 방식이 동작하는 이유는 class의 클래스가 제네릭이기 때문이다. class 리터럴 타입은 Class
가 아닌 Class<T>
이다.
Favorites 인스턴스는 타입 안전하다. String을 요청했는데 Integer를 반환하는 일은 절대없다. 또한 모든 키의 타입이 제각각이라 일반적인 맵과 달리 여러 가지 타입의 원소를 담을 수 있다. 따라서 Favorites는 타입 안전 이종 컨테이너라 할 만하다.
Favorites의 private Map variable을 보자. Map<Class<?>, Object>이다. 비한정적 와일드카드 타입이라 이 맵 안에 아무것도 넣을 수 없다고 생각할 수 있지만 그 반대다. 와일드카드 타입이 중첩(nested) 되었다는 점을 깨달아야 한다. 맵이 아니라 키가 와일드카드 타입인 것이다. 이는 모든 키가 서로 다른 매개변수화 타입일 수 있다는 뜻으로 첫번째는 Class, 두번째는 Class식으로 될 수 있다. 다양한 타입을 지원하는 힘은 여기서 나온다.
키와 값 사이의 타입 링크(type link)는 넣는 순간 사라지지만 getFavorite에서 이를 되살린다. cast 메서드를 이용해서 객체 참조를 Class 객체가 가리키는 타입으로 동적 형 변환을 수행한다.
cast 메소드는 주어진 인수가 Class 객체가 알려주는 타입의 인스턴스인지를 검사하고, 맞으면 인수를 아니면 ClassCastException을 던진다.
public class Class<T> {
T cast(Object obj);
}
위 코드에서 보듯 cast 의 반환 타입은 Class 객체의 타입 매개변수와 같다. 이것이 정확히 getFavorite메소드에 필요한 기능으로, T 로 비검사 형변환을 하는 손실없이도 Favorites를 타입 안전하게 만드는 비결이라고 할 수 있다.
이 Favorites 클래스엔 두 가지 제약사항이 있다. 첫번째는 악의적인 클라이언트가 Class 객체를 로 타입으로 넘기면 Favorites 인스턴스의 타입 안전성이 쉽게 깨진다는 점이다. 하지만 이렇게 짜여진 클라이언트 코드에서는 비검사 경고가 뜰 것이다. 다음 코드를 실행하면 ClassCastException 을 던진다.
f.putFavorite((Class)Integer.class, "Integer의 인스턴스가 아닙니다.");
int favoriteInteger = f.getFavorite(Integer.class);
이것은 HashSet, HashMap 등의 일반 컬렉션도 가지고 있는 문제다. HashSet의 로 타입을 사용하면 HashSet에 String을 넣는건 아주 쉽다.
HashSet<Integer> set = new HashSet<>();
((HashSet)set).add("string");
Favorites 가 타입 불변식을 어기는 일이 없도록 보장하기 위한 방법은 간단하다. 동적 형변환을 사용하면 된다.
public <T> void putFavorite(Class<T> type, T instance) {
favorites.put(Objects.requireNonNull(type), Objects.requireNonNull(type.cast(instance)));
}
두번째 제약은, 실체화 불가 타입에는 사용할 수 없다는 것이다.
즉, String
이나 String[]
은 사용할 수 있어도 List<String>
이나 List<Integer>
는 저장할 수 없다는 이야기다.
List<String>
를 저장하려고 해도 컴파일되지 않는다. List<String>
용 Class 객체를 얻을 수 없기 때문이다. (List<String>.class
-> 문법 오류가 난다)
그 이유는 List<String>
이나 List<Integer>
나 List.class
라는 같은 Class 객체를 공유하기 때문이다.
Favorites 가 사용하는 타입 토큰은 비한정적이다. 즉, getFavorites와 putFavorites는 어떤 Class 객체든 받아들인다는 뜻이다. 타입을 제한하고 싶을 때는 한정적 타입 토큰을 사용하면 된다.
@Override
public <A extends Annotation> A[] getAnnotationsByType(Class<A> annotationClass) {
Objects.requireNonNull(annotationClass);
AnnotationData annotationData = annotationData();
return AnnotationSupport.getAssociatedAnnotations(annotationData.declaredAnnotations,
this,
annotationClass);
}
annotationClass 인수는 어노테이션 타입을 뜻하는 한정적 타입 토큰이다. 이 메서드는 토큰으로 명시한 타입의 어노테이션이 대상 요소에 달려있다면 그 어노테이션을 반환하고 없다면 null 을 반환한다.
Class<?>
타입의 객체가 있고 이를 getAnnotaion 처럼 한정적 타입 토큰을 받는 메서드에 넘기려면 어떻게 해야 할까?
객체를 Class<? extends Annotation>
으로 형 변환 할수도 있지만 이 형변환은 비검사이므로 컴파일하면 경고가 뜰 것이다.
이 때는 asSubclass
라는 메서드를 사용하면 된다. 호출된 인스턴스 자신의 Class객체를 인수가 명시한 클래스로 형 변환한다.
static Annotation getAnnotation(AnnotatedElement element, String annotationTypeName) {
Class<?> annotationType = null; // 비한정적 타입 토큰
try {
annotationType = Class.forName(annotationTypeName);
} catch (Exception ex) {
throw new IllegalArgumentException(ex);
}
return element.getAnnotation(
annotationType.asSubclass(Annotation.class));
}
asSubclass
메소드를 사용해 런타임에 읽어내는 예이다.