제네릭은 컬렉션과 단일원소 컨테이너에 흔히 쓰인다. 이때 매개변수화되는 대상은 원소가 아닌 컨테이너 자신이다. 따라서 하나의 컨테이너에서 매개변수화할 수 있는 타입의 수가 제한된다.
하지만 더 유연한 수단
이 필요할 때도 종종 있다.
예를 들어, 데이터베이스의 행은 임의 개수의 열을 가질 수 있는데 모두 열을 타입 안전하게 이용할 수 있다면 멋질 것이다.
해법은
1️⃣ 컨테이너 대신 키를 매개변수화한 다음,
2️⃣ 컨테이너에 값을 넣거나 뺄 때 매개변수화한 키를 함께 제공하는 것이다.
이러한 설계 방식을 타입 안전 이종 컨테이너 패턴
(type safe heterogeneous container pattern)이라고 한다.
즐겨찾는 인스턴스를 저장하고 검색할 수 있는 Favorites 클래스 예제를 보자.
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));
}
}
Class<?>
타입 토큰
이라고 한다.타입 토큰
(type token): 컴파일타임 타입 정보와 런타임 타입 정보를 알아내기 위해 메서드들이 주고받는 class 리터럴.비한정적 와일트카드 타입
을 사용하여, 모든 키가 서로 다른 매개변수화 타입일 수 있도록 한다.Object
Object
를 사용하여, 키와 값 사이의 타입 관계를 보증하지 않는다.putFavorite
메서드getFavorite
메서드cast
메서드를 사용해 객체 참조를 Class 객체가 가리키는 타입으로 동적 형변환한다.cast
메서드ClassCastException
을 던진다.public class Class<T> {
T cast(Object obj);
}
이를 사용하는 클라이언트 코드는 다음과 같다.
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());
// 출력 결과: Java cafebabe Favorites
}
1. Class 객체를 (제네릭이 아닌) 로 타입으로 넘기면 Favorites 인스턴스의 타입 안전성이 쉽게 깨진다.
다음에 나오는 동적 형변환을 통해 해결 가능하다.
2. 실체화 불가 타입에는 사용할 수 없다.
예를 들어, String, String[]은 저장할 수 있지만 List<String>은 저장할 수 없다.
이 제약에 대한 완벽히 만족스러운 우회로는 없다.
Favorites 클래스에는 Class 객체를 (제네릭이 아닌) 로 타입으로 넘기면 Favorites 인스턴스의 타입 안전성이 쉽게 깨진다는 제약이 존재한다.
동적 형변환을 통해 인수로 주어진 인스턴스의 타입이 type으로 명시한 타입과 같은지 확인하면 된다!
public <T> void putFavorite(Class<T> type, T instance) {
favorites.put(Objects.requireNonNull(type), type.cast(instance));
}
+) java.utils.Collections에는 이 방식을 적용한 컬렉션 래퍼들이 존재한다.
checkedSet
, checkedList
, checkedMap
메서드
Favorites가 사용하는 타입 토큰은 비한정적이다.
그러나 때로는 이 메서드들이 허용하는 타입을 제한하고 싶을 수 있는데, 이때는 한정적 타입 토큰
을 활용할 수 있다.
💡 한정적 타입 토큰
한정적 타입 매개변수
나한정적 와일드카드
를 사용하여 표현 가능한 타입을 제한하는 타입 토큰이다.
애너테이션 API는 한정적 타입 토큰을 적극적으로 사용한다.
public <T extends Annotation> T getAnnotation(Class<T> annotationType);
메서드에 넘기기 위해서는 객체를 Class<? extends Annotation>
으로 형변환해야 한다.
운 좋게도, Class 클래스는 이런 형변환을 안전하고 동적으로 수행해주는 메서드를 제공한다.
asSubclass
메서드
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));
}
📌 핵심 정리
- 컬렉션 API로 대표되는 일반적인 제네릭 형태에서는 한 컨테이너가 다룰 수 있는 타입 매개변수의 수가 고정되어 있다.
- 하지만 컨테이너 자체가 아닌 키를 타입 매개변수로 바꾸면 이런 제약이 없는
타입 안전 이종 컨테이너
를 만들 수 있다.타입 안전 이종 컨테이너
는 Class를 키로 쓰며, 이런 식으로 쓰이는 Class 객체를타입 토큰
이라 한다.- 또한, 직접 구현한 키 타입을 쓸 수 있다.
- 예컨대 데이터베이스의 행(컨테이너)를 표현한
DatabaseRow
타입에는 제네릭 타입인Column<T>
를 키로 사용할 수 있다.