아이템33 타입 안전 이종 컨테이너를 고려하라
Effective Java 3e 아이템 33를 요약한 내용 입니다.
제네릭은 Set<E>
, Map<K,V>
등의 컬렉션과 ThreadLocal<T>
, AtomicReference<T>
등의 단일 원소 컨테이너에도 흔히 쓰인다. 예컨대 Set
에는 원소의 타입을 뜻하는 단 하나의 타입 매개변수만 있으면 되며, Map
에는 키와 값의 타입을 뜻하는 2개만 필요한 식이다.
더 유연한 수단이 필요할 때도 종종 있다.
데이터베이스의 행(row)
은 임의 개수의 열(column)
을 가질 수 있는데, 모두 열을 타입 안전하게 이용할 수 있다면 멋질 것이다.
다행히 쉬운 해법이 있다.
컨테이너
대신 키를 매개변수화한 다음, 컨테이너에 값을 넣거나 뺄 때 매개변수화
한 키를 함께 제공하면 된다. 이런 설계 방식을 타입 안전 이종 컨테이너 패턴
이라 한다.
public class Favorite {
private Map<Class<?>, Object> favorites = new HashMap<>();
public <T> void putFavorite(Class<T> type, T instance) {
favorites.put(Objects.requireNonNull(type), type.cast(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());
}
Favorites 인스턴스는 타입 안전하다. 따라서 Favorites는 타입 안전 이종 컨테이너
라 할 만하다.
비한정적 와일드카드 타입
이라 이 맵 안에 아무것도 넣을 수 없다고 생각할 수 있지만, 사실은 그 반대다. 와일드카드 타입이 중첩 되었다는 점을 깨달아야 한다.
와일드 타입이 중첩되었다는 말은 무슨 뜻일까?
이는 모든 키가 서로 다른 매개변수화타입일 수 있다는 뜻으로, 첫 번째는 Class<String>, 두 번째는 Class<Integer>식으로 될 수 있다.
Favorite
클래스에는 알아두어야 할 제약이 두 가지 있다.
Favorite
클래스에는 알아두어야 할 제약이 두 가지 있다.악의적인 클라이언트가 Class 객체를 (제네릭이 아닌)
로 타입
(아이템 26)으로 넘기면 Favorite 인스턴스의타입 안전성
이 쉽게 깨진다. 하지만 이는 클라이언트 코드에서 컴파일할 때비검사 경고
가 뜰 것이다.Favorites가
타입 불변식
을 어기는 일이 없도록 보장하려면putFavorite
메서드와 같이instance
의 타입이type
으로 명시한 타입과 같은지 확인하면 된다.java.util.Collections
에는checkedSet
,checkedList
,checkedMap
같은 메서드가 있는데 바로 이 방식을 적용한 컬렉션 래퍼들이다.Favorites 클래스의 두 번째 제약은
실체화 불가 타입
에는 사용할 수 없다는 것이다. 다시 말해, 즐겨 찾는String
이나String[]
은 저장할 수 있어도 즐겨 찾는 List<String>은 저장할 수 없다.List<String>
을 저장하려는 코드는 컴파일되지 않을 것이다.List<String>
용Class
객체를 얻을 수 없기 때문이다.List<String>.class
라고 쓰면 문법 오류가 난다.List<String>
과List<Integer>
는List.class
라는 객체를 공유하기 때문이다.이는
한정적 타입 토큰
을 활용하면 가능하다. 한정적 타입 토큰이란 단순히 한정적 타입 매개변수나 한정적 와일드카드를 사용하여 표현 가능한 타입을 제한하는 타입 토큰이다.애너테이션 API(아이템 39)는 한정적 타입 토큰을 적극적으로 사용한다.
public <T extends Annotation> T getAnnotation(Class<T> annotationType);
여기서
annotationType
인수는 애너테이션 타입을 뜻하는 한정적 타입 토큰이다. 이 메서드는 토큰으로 명시한 타입의 에너테이션이 대상 요소에 달려 있다면 그 애너테이션을 반환하고 없다면null
을 반환한다. 즉, 애너테이션된 요소는 그 키가 애너테이션 타입인, 타입 안전 이종 컨테이너이다.
Class<?>
타입의 객체가 있고, 이를 (getAnnotation
처럼) 한정적 타입 토큰을 받는 메서드에 넘기려면 어떻게 해야 할까?
객체를 Class<? extends Annotation>
으로 형변환할 수도 있지만 이 형변환은 비검사
이므로 컴파일하면 경고가 뜻 것이다. 운 좋게도 Class
클래스가 이런 형변환을 안전하게 수행해주는 인스턴스 메서드를 제공한다. 바로 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));
}
정리
컬렉션 API
로 대표되는 일반적인 제네릭 형태에서는 한 컨테이너가 다룰 수 있는 타입 매개변수
의 수가 고정되어 있다. 하지만 컨테이너 자체가 아닌 키를 타입 매개변수
로 바꾸면 이런 제약이 없는 타입 안전 이종 컨테이너
를 만들 수 있다. 타입 안전 이종 컨테이너는 Class
를 키로 쓰며, 이런 식으로 쓰이는 Class
객체를 타입 토큰이라 한다.
Last updated
Was this helpful?