컬렉션 API로 대표되는 일반적인 제네릭 형태에서는 한 컨테이너가 다룰 수 있는 타입 매개변수의 수가 고정되어 있다. 하지만 컨테이너 자체가 아닌 키를 타입 매개변수로 바꾸면 이런 제약이 없는 type safe heterogeneous container를 만들 수 있다. 이는, Class를 키로 쓰며, 이런 식으로 쓰이는 Class 객체 를 타입 토큰이라 한다.
또한 직접 구현한 키 타입도 쓸 수 있다. 예컨대 데이터베이스의 행(컨테이너)을 표현한 DatabaseRow 타입에는 제네릭 타입인 Column<T>를 키로 사용할 수 있다.
type safe heterogeneous container 패턴cast 메서드알아두어야 할 제약한정적 타입 토큰예시 : 애너테이션 API(아이템 39)asSubclass 메서드 - 호출된 인스턴스 자신의 Class 객체를 인수가 명시한 클래스로 형변환 한다(형변환 된다는 것은 이 클래스가 인수로 명시한 클래스의 하위 클래스라는 뜻)
type safe heterogeneous container 패턴
- class 리터럴의 타입은 Class가 아닌 Class<T> 다.
- 예컨대, String.class 의 타입은 Class<String>
- Integer.class의 타입은 Class<Integer>
public class Favorites { private Map<Class<?>, Object> favories = 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)); } }
- 비한정적 와일드카드 타입이라 이 맵 안에 아무것도 넣을 수 없다고 생각할 수 있지만, 사실은 그 반대다.
- 와일드카드 타입이 nested 되었다는 점을 깨달아야 한다.
- 이는 모든 키가 서로 다른 매개변수화 타입일 수 있다는 뜻으로, 첫 번째는 Class<String>, 두 번째는 Class<Integer>식으로 될 수 있다. 다양한 타입을 지원하는 힘이 여기서 나온다.
- 다음으로 알아둘 점은, 값 타입은 단순히 Object라는 것. 즉, 키와 값 사이의 타입 관계를 보증하지는 못함
- 사실 자바의 타입 시스템에서는 이 관계를 명시할 방법이 없다. (putFavorite에서 타입 링크 정보는 버려짐)
- 하지만 getFavorite 메서드에서 이 관계를 되살릴 수 있으니 상관없음.
- cast 메서드는 형변환 연산자의 동적 버전임
cast 메서드
public class Class<T> { T cast(Object obj); }
- cast 메서드의 시그니처가 Class 클래스가 제네릭이라는 이점을 완벽히 활용하기 때문에 이를 활용함
- 비검사 형변환을 하는 손실 없이 Favorites를 타입 안전하게 만드는 비결임
알아두어야 할 제약
- 악의적인 클라이언트가 Class 객체를 (제네릭이 아닌) 로 타입으로 넘기면 Favorites 인스턴스의 타입 안전성이 쉽게 깨진다.
- 이를 보장하려면 putFavorite 메서드에서 인수로 주어진 instance의 타입이 type으로 명시한 타입과 같은지 확인하면 됨
f.putFavorite((Class)Integer.class, "Integer의 인스턴스가 아닙니다."); // 이렇게 넣으면 Class<T>가 아니라 Class로 들어가기 때문에 T를 알 수 없고, T instance와 // 맞는지 안맞는지 타입 체크가 불가능해진다는 말임
public <T> void putFavorite(Class<T> type, T instance) { favorites.put(Objects.requireNonNull(type), type.cast(instance)); }
- 실체화 불가 타입에는 사용할 수 없다
- String 이나 String[]은 저장할 수 있어도 즐겨 찾는 List<String>은 저장할 수 없다.
- List<String>과 List<Integer>는 List.class라는 같은 Class 객체를 공유하므로 둘다 허용하게 되어 저장이 가능하게 되면 Favorites 객체의 내부는 아수라장이 된다.
- 이 두 번째 제약을 슈퍼 타입 토큰(super type token)으로 해결하려는 시도도 있음.
- 슈퍼 타입 토큰은 자바 업계의 거장인 닐 개프터가 고안한 방식으로, 실제로 아주 유용하며 스프링 프레임워크에서는 아예
ParameterizedTypeReference
라는 클래스로 미리 구현해 놓았음
한정적 타입 토큰
- Favorites가 사용하는 타입 토큰은 비한정적이다. 즉, getFavorite과 putFavorite 은 어떤 Class 객체든 받아들인다.
- 때로, 이 메서드들이 허용하는 타입을 제한하고 싶을 수 있는데, 한정적 타입 토큰을 활용하면 가능하다.
예시 : 애너테이션 API(아이템 39)
public <T extends Annotation> T getAnnotation(Class<T> annotationType);
- 위 예시는 AnnotatedElement 인터페이스에 선언된 메서드로, 대상 요소에 달려 있는 애너테이션을 런타임에 읽어 오는 기능을 한다.
- 이 메서드는 토큰으로 명시한 타입의 애너테이션이 대상 요소에 달려 있다면 그 애너테이션을 반환하고 없다면 null을 반환한다.
- Class<?> 타입의 객체가 있고, 이를 한정적 타입 토큰을 받는 메서드에 넘기려면 어떻게 해야 할까?
- 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)); }