제네릭은 하나의 컨테이너에서 매개변수화할 수 있는 타입의 수가 제한된다고 합니다.
예를 들어, Set<E>에는 원소의 타입을 뜻하는 단 하나의 타입 매개변수만 있으면 되며,
Map<K,V> 에는 키와 값을 뜻하는 2개만 필요합니다.
컨테이너 대신 "키"를 매개변수화한 다음, 컨테이너에 값을 넣거나 뺄 때 매개변수화 한
키를 함께 제공한다면 더 유연하게 여러 타입을 받을 수 있지 않을까?
하는 생각에서 나온 설계 방식을, 타입 안전 이종 컨테이너 패턴이라고 부릅니다.
제네릭은 Set이나 Map과 같은 컬렉션들은 하나의 원소만을 담는 컨테이너에 가장 많이 쓰입니다.
이때 보통의 경우 컨테이너별로 매개변수화 할 수 있는 타입의 수는 하나로 제한 된다는 것을 알려주고 있습니다
이때, 타입의 수가 하나로 제한되는 것은 컨테이너의 제네릭 타입이 정해졌을 때,
해당 타입만으로 자료형이 제한되는 것을 의미한다고 합니다.
Set<Integer> intSet = new HashSet<>();
Map<Integer, String> map = new HashMap<>();
그런데, 이런식으로 하면 되는게 아닌가? 하는 의문점이 생길 수 있습니다.
Set<Object> set = new HashSet<>();
위 방법대로 사용할 경우 해당 컨테이너에서 얻은 객체가 어떤 타입인지 정확히 알 수 없으며,
로직을 이해하고 캐스팅해 사용해도 컴파일 시에는 못잡아낸 런타임 오류가 발생할 수 있습니다.
이렇게도 구현할 수 있습니다.
Map<Class<?>, Object> favorites = new HashMap<>();
favorites.put(String.class, "안녕!");
favorites.put(Integer.class, 123);
String item = (String) favorites.get(String.class);
Map<Class<?>, Object> favorites = new HashMap<>();
favorites.put(Integer.class, "안녕!");
Integer item = (Integer) favorites.get(Integer.class); // ClassCastException 발생
하지만! 인스턴스 저장 시 key와 인스턴스의 자료형을 다르게 저장할 경우
ClassCastException이 발생하기 때문에 타입 안정성이 떨어집니다.
앞선 케이스에서 문제가 되었던 타입 안정성을 보장하기 위해선, 타입 안전 이종 컨테이너를 사용하면 됩니다.
public class Favorites {
private Map<Class<?>, Object> favorites = new HashMap<>();
public <T> void put(Class<T> type, T instance) {
favorites.put(Object.requireNonNull(type), instance);
}
public <T> T getFavorite(Class<T> type) {
return type.cast(favorites.get(type));
}
}
public class Main {
public static void main(String[] args) {
Favorites favorites = new Favorites();
favorites.put(String.class, "안녕");
favorites.put(Integer.class, "테스트"); // 컴파일 에러 발생
String item = favorites.get(String.class);
}
}
타입 안전 이종 컨테이너도 타입 안정성에 완벽하지는 않습니다.
// raw 타입으로 넘기면 타입 안정성이 깨짐!
favorites.put((Class) Integer.class, "안녕");
악의적인 클라이언트가 Class 객체를 Raw 타입으로 넘기면 컴파일 시 비검사 경고가 뜨지만, 타입 안정성은 깨집니다.
public <T> void put(Class<T> type, T instance) {
favorites.put(Object.requireNonNull(type), type.cast(instance));
}
타입 안전 이종 컨테이너는 실체화 불가 타입에는 사용할 수 없습니다.
String이나 String[]은 가능하지만, List은 사용할 수 없습니다.
List 과 List는 모두 List.class이기 때문에 Favorites 객체는 큰일나게 될수도 있다는 것입니다.
Favorites가 사용하는 타입 토큰은 비한정적입니다.
때로는 이 메서드들이 허용하는 타입을 제한하고 싶을 수도 있는데, 한정적 타입 토큰을 활용하면 됩니다.
한정적 타입 토큰이란 단순히 한정적 타입 매개변수나 한정적 와일드카드를 사용하여
표현 가능한 타입을 제한하는 타입 토큰입니다.
애너테이션 API는 한정적 타입 토큰을 적극적으로 사용합니다.
예를 들어 다음은 AnnotatedElement 인터페이스에 선언된 메서드로,
대상 요소에 달려 있는 애너테이션을 런타임에 읽어 오는 기능을 합니다.
이 메서드는 리플렉션의 대상이 되는 타입들, 즉 클래스(java.lang.Class), 메서드, 필드 같이
프로그램 요소를 표현하는 타입들에서 구현합니다.
public <T exnteds Annotation> T getAnnotation(Class<T> annotationType);
annotationType 인수는 애너테이션 타입을 뜻하는 한정적 타입 토큰입니다.
위 메서드는 토큰으로 명시한 타입의 애너테이션이 대상 요소에 달려 있다면
그 애너테이션을 반환하고, 없다면 null을 반환합니다.
즉, 애너테이션된 요소는 그 키가 애너테이션 타입인, 타입 안전 이종 컨테이너인 것입니다.
import java.lang.annotation.*;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface FooAnnotation {
String test();
}
public class Foo {
@FooAnnotation(test = "StringTest")
private String value;
public Foo(String value) {
this.value = value;
}
}
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
public class AnnotationExam {
public static void main(String[] args) throws NoSuchFieldException {
Field field = Foo.class.getDeclaredField("value");
Class<?> annotationType = null;
try {
annotationType = Class.forName("effective_java.item_33.FooAnnotation");
} catch (ClassNotFoundException e) {
throw new IllegalArgumentException();
}
Annotation annotation = field.getAnnotation(
annotationType.asSubclass(Annotation.class));
System.out.println(annotation.toString());
}
}
@FooAnnotation(test = "StringTest")
Class<?> 타입의 객체가 있고, 이를 한정적 타입 토큰을 받는 메서드에 넘기기 위해서는
객체를 Class<? extends Annotation>으로 형변환할 수도 있지만,
이 형변환은 비검사이므로 컴파일 경고가 발생할 것입니다.
Class 클래스는 이런 형변환을 안전하게 (그리고 동적으로) 수행해주는 인스턴스 메서드를 제공합니다.
바로 asSubclass() 메서드로, 호출된 인스턴스 자신의 Class 객체를 인수가 명시한 클래스로 형변환합니다.
형변환된다는 것은 이 클래스가 인수로 명시한 클래스의 하위 클래스라는 뜻입니다.
형변환에 성공하면 인수로 받은 클래스 객체를 반환하고, 실패하면 ClassCastException을 던집니다.
컬렉션 API로 대표되는 일반적인 제네릭형태는 한 컨테이너가 다룰수 있는 매개변수의 수가 고정적입니다.
하지만 컨테이너자체가 아닌 키를 타입 매개변수로 바꾸면 이런 제약이 없는 타입 안전 이종 컨테이너를 만들 수 있다.
타입 이종 컨테이너는 Class를 키로 사용하며, 이런 식으로 쓰이는 Class 객체를 타입 토큰이라 한다.
또한 직접 구현한 키 타입도 쓸 수 있다.
예컨데 DB의 행(컨테이너)을 표현한 DatabaseRow 타입에는 제네릭 타입인 Column를 사용할 수 있다.
하지만 타입이종 컨테이너를 사용하는데 제약이 있으니 이런 제약들을 주의해서 사용하자.