- 타입 안전 이종 컨테이너는 영어로
type safe heterogeneous container
이다.heterogeneous
는여러 다른 종류들로 이뤄진
이라는 뜻이다.
- 즉, 타입 안전 이종 컨테이너란
여러 다른 종류들로 이루어진 값을 저장하는 타입 안전한 객체
를 의미한다.
- 제네릭은 컬렉션과 단일원소 컨테이너에 흔히 쓰인다.
- 이 때 매개변수화되는 대상은 원소가 아닌 컨테이너 자신이다.
- 따라서 하나의 컨테이너에서 매개변수화할 수 있는 타입의 수가 제한된다.
- ex) Set, Map<String, Integer>
- 하지만 유연한 수단이 필요할 때도 있다.
- ex) 데이터베이스의 모든 열을 타입 안전하게 이용하고 싶다.
- 컨테이너 대신 키를 매개변수화한 다음, 컨테이너에 값을 넣거나 뺄 때 매개변수화한 키를 함께 제공한다.
public class Favorites {
private Map<Class<?>, Object> favorites = new HashMap<>();
public <T> void putFavorite(Class<T> type, T instance) {
favorites.put(Objects.requireNonNull(type), 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 객체와 즐겨찾기 인스턴스를 favorites에 추가하여 관계를 짓는다.
- 이 때 키와 값 사이의 '타입 링크' 정보는 버려진다.
- 즉, 그 값이 그 키 타입의 인스턴스라는 정보가 사라진다.
- 주어진 Class 객체에 해당하는 값을 favorites 맵에서 꺼낸다.
- 이 객체가 반환해야할 타입 객체는 맞지만, 잘못된 컴파일타임 타입(Object)를 가지고 있어 T 타입으로 바꿔야 한다.
- Class의
cast()
메서드를 사용해 객체 참조를 Class 객체가 가리키는 타입으로 동적 형변환한다.
- cast 메서드의 시그니처가 Class 클래스가 제네릭이라는 이점을 완벽히 활용한다.
- cast의 반환 타입은 Class 객체의 타입 매개변수와 같다.
public class Class<T> {
@SuppressWarnings("unchecked")
public T cast(Object obj) {
if (obj != null && !isInstance(obj))
throw new ClassCastException(cannotCastMsg(obj));
return (T) obj;
}
}
- 따라서 Favorites를 T로 비검사 형변환하지 않고도 타입 안전하게 만들 수 있다.
- Class 객체를 제네릭이 아닌 로 타입으로 넘기면 Favorites 인스턴스의 타입 안전성이 쉽게 깨진다.
- 예를 들어 아래의 코드는 동작한다.
HashSet<Integer> tmp = new HashSet<>(); ((HashSet)tmp).add("하이");
- 해당 문제는 동적 형변환을 통해 해결할 수 있다.
- 실체화 불가 타입은 사용할 수 없다.
- 예를 들어 String 이나 String[ ]은 저장할 수 있어도 즐겨찾는 List은 저장할 수 없다.
- List용 Class 객체를 얻을 수 없기 때문이다.
- 이 제약에 대한 완벽한 우회로는 없다.
- 동적 형변환을 통해 인수로 주어진 인스턴스의 타입이 type으로 명시한 타입과 같은지 확인하면 된다.
public <T> void putFavorite(Class<T> type, T instance) {
favorites.put(Objects.requireNonNull(type), type.cast(instance));
}
- Favorites가 사용하는 타입 토큰은 비한정적이다.
- 즉 getFavorite과 putFavorite은 어떤 Class든 받아들인다.
- 때로는 이 메서드들이 허용하는 타입을 제한하고 싶을 수 있는데, 이 때는
한정적 타입 토큰
을 사용하면 된다. - 한정적 타입 토큰이란,
한정적 타입 매개변수
나한정적 와일드카드
를 사용하여 표현 가능한 타입을 제한하는 타입 토큰이다. - 어노테이션 API는 한정적 타입 토큰을 적극적으로 사용한다.
public <T extends Annotation> T getAnnotation(Class<T> annotationType);
- annotationType 인수는 애너테이션 타입을 뜻하는 한정적 타입 토큰이다.
- 애너테이션된 요소는 키가 애너테이션 타입인 타입 안전 이종 컨테이너다.
Q. 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));
}