제네릭은 Set<E>
, Map<K, V>
등의 컬렉션과 ThreadLocal<T>
, AtomicReference<T>
등의 단일원소 컨테이너에도 흔히 쓰인다. 이런 모든 쓰임에서 매개변수화되는 대상은 컨테이너 자신이다. 따라서 하나의 컨테이너에서 매개변수화할 수 있는 타입의 수는 제한된다. 예를 들어, Set
에는 우너소의 타입을 뜻하는 단 하나의 타입 매개변수만 있으면 되는 것이다.
하지만 더 유연한 수단이 필요할 때도 종종 있다. 예를 들어, 데이터베이스의 행은 임의 개수의 열을 가질 수 있는데, 모두 열을 타입 안전하게 이용할 수 있다면 좋다. 쉬운 해법으로는 컨테이너 대신 키를 매개변수화한 다음, 컨테이너에 값을 넣거나 뺄 때 매개변수화한 키를 함께 제공하면 되는 것이다. 이렇게 하면 제네릭 타입 시스템이 값의 타입이 키와 같은을 보장해준다. 이러한 설계 방식을 타입 안전 이종 컨테이너 패턴이라 한다.
간단한 예로 타입별로 즐겨 찾는 인스턴스를 저장하고 검색할 수 있는 Favorites
클래스를 들어본다. 각 타입의 Class
객체(String.class
, Integer.class
)를 매개변수화한 키 역할로 사용하면 되는데, 이 방식이 동작하는 이유는 class
의 클래스가 제너릭이기 때문이다. class
의 리터럴의 타입은 Class
가 아닌 Class<T>
다. 예를 들어 String.class
의 타입은 Class<String>
이고 Integer.class
의 타입은 Class<Integer>
인 식이다. 한편, 컴파일타임 타입 정보와 런타임 타입 정보를 알아내기 위해 메서드들이 주고받는 class
리터럴을 타입 토큰이라 한다. Favorites
클래스의 API
는 아래와 같다.
public class Favorites{
public <T> void putFavorites(Class<T> type, T instance);
public <T> T getFavorites(Class<T> type);
Favorites
클래스의 API
를 사용하는 예시 코드다.
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", favorieString, favorieInteger, favoriteClass.getName();
}
Favorites
인스턴스는 타입 안전하다. 또한 모든 키의 타입이 제각각이라, 일반적인 맵과 달리 여러 가지 타입의 원소를 담을 수 있다. 따라서 Favorites
는 타입 안전 이종 컨테이너다.
Favorites
의 구현은 다음과 같다.
public class Favorites{
private Map<class<?>, Object> favorites=new HashMap<>();
public <T> void putFavorites(Class<T> type, T instance){
favorites.put(Objects.requireNonNull(type), instance);
}
public <T> T getFavorites(Class<T> type){
return type.cast(favorites.get(type));
}
}
위 코드에서 키가 와일드카드 타입인 것을 알 수 있다. 이는 모든 키가 서로 다른 매개변수화 타입일 수 있다는 뜻으로, 첫 번째는 Class<String>
, 두 번째는 Class<Integer>
식으로 될 수 있다. 다양한 타입을 지원하는 힘이 여기서 나온다. 또한 favorites
맵의 값 타입은 Object
라는 것이다. 이 맵은 키와 값 사이의 타입 관계를 브증하지 않는다는 뜻이다.
putFavorites
의 구현은 주어진 Class
객체와 즐겨찾기 인스턴스를 favorites
에 추가해 관계를 지으면 된다. 앞에서 얘기했듯이 키와 값 사이의 타입 링크
정보는 사라진다. 하지만 getFavorite
메서드에서 이관계를 되살릴 수 있다.
getFavorite
의 구현은 다음과 같다. 먼저, 주어진 Class
객체에 해당하는 값을 favorites
맵에서 꺼낸다. 이 객체가 반환해야 할 객체가 맞지만, 잘못된 컴파일타임 타입을 가지고 있다. 이 객체의 타입은 (favorites
맵의 값 타입인)Object
이나, 이를 T
로 바꿔 반환해야 한다. 따라서 getFavorite
구현은 Class
의 cast
메서드를 사용해 이 객체 참조를 Class
객체가 가리키는 타입으로 동적 변환한다.
cast
메서드는 형변환 연산자의 동적 버전이다. 이 메서드는 주어진 인수가 Class
객체가 알려주는 타입의 인스턴스인지를 검사한 다음, 맞다면 그 인수를 그대로 반환하고, 아니면 ClassCastException
을 던진다. 클라이언트 코드가 깔끔히 컴파일된다면 getFavorite
이 호출하는 cast
는 CLassCastException
을 던지지 않을 것이다.(putFavorite
메서드의 시그니처를 보면 Class<T> type
, T instacne
로 타입 매개변수가 같음.) 그럼에도 cast
를 사용하는 이유는 cast
의 메서드의 시그니처가 Class
클래스가 제네릭이라는 이점을 완벽히 활용하기 때문이다.
public class Class<T>{
T cast(Object obj);
지금부터 Favorites
클래스의 두 가지 제약에 대해서 알아본다.
Class
객체를 로 타입으로 넘기면 Favorites
인스턴스의 타입 안전성이 쉽게 깨진다.f.putFavorite((Class)Integer.class, "No Integer 인스턴스");
int favoriteInteger=f.getFavorite(Integer.class);
그러나 이렇게 짜여진 클라이언트 코드에서는 컴파일할 때 비검사 경고가 뜰 것이다. 이 정도의 문제를 감수하면 런타임 타입 안전성을 얻을 수 있다. Favorites
가 타입 불변식을 어기는 일이 없도록 하려면 putFavorite
메서드에서 인수로 주어진 instance
의 타입이 type
으로 명시한 타입과 같은지 확인하면 된다.public <T> void putFavorite(Class<T> type, T instance){
favorites.put(Objects.requireNonnNull(type), type.cast(instance));
Favorites
클래스이 두 번째 제약은 실체화 불가 타입(런타임 시에는 컴파일타임보다 타입 정보를 적게 가지고 있는 타입)에는 사용할 수 없다는 것이다. 다시 말해, 즐겨 찾는 String
이나 String[]
은 저장할 수 있어도 즐겨 찾는 List<String>
은 저장할 수 없다. 만약 List<String>
과 List<Integer>
의 저장을 허용하면, List.class
라는 같은 클랙스 객체를 공유하므로 Favorites
객체의 내부는 아수라장이 될 것이다. 이 두 번째 제약에 대한 완벽히 만족스러운 우회로는 없다.Favorites
가 사용하는 타입 토큰은 비한정적이다. 즉, getFavorite
과 putFavorite
은 어떤 Class
객체든 받아들인다. 때로는 이 메서드들이 허용하는 타입을 제한하고 싶을 수 있다. 이 때는 한정적 타입 토큰을 활용하면 가능하다.
애너테이션 API
는 한정적 타입 토큰을 적극적으로 사용한다. 예를 들어 AnnotatedElement
인터페이스에 선언된 메서드로, 대상 요소에 달려 있는 애너테이션을 런타임에 읽어 오는 기능을 한다. 이 메서드는 리플렉션의 대상이 되는 타입들, 즉 클래스(java.lang.class<T>
), 메서드(java.lang.reflect.Method
), 필드(java.lang.reflect.Field
)같이 프로그램 요소를 표현하는 타입들에서 구현한다.
public <T extends Annotation>
T getAnnotation(Class<T> annotationType);
여기서 annotationType
인수는 애너테이션 타입을 뜻하는 한정적 타입 토큰이다. 이 메서드는 토큰으로 명시한 타입의 애너테이션이 대상 요소에 달려 있다면 그 애너테이션을 반환하고, 없다면 null
을 반환한다. 즉, 애너테이션된 요소는 그 키가 애너테이션 타입인, 타입 안전 이종 컨테이너인 것이다.
Class<?>
타입의 객체가 있고, 이를 한정적 타입 토큰을 받는 메서드에 넘기려면 다음과 같이 하면 된다. 객체를 Class<? extends Annotation>
으로 형변환할 수도 있지만, 이 형변환은 비검사이므로 컴파일하면 경고가 뜰 것이다. Class
크래스가 이런 형변환을 안전하게 동적으로 수행하는 asSubclass
메서드가 있다. 호출된 인스턴스 자신의 Class
객체를 인수가 명시한 클래스로 형변환한다. 형변환된다는 것은 이 클래스가 인수로 명시한 클래스의 하위 클래스인 것이다. 형변환에 성공하면 인수로 받은 클래스 객체를 반환하고, 실패하면 ClassCastException
을 던진다.
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));
}