이종 컨테이너 패턴 (type safe heterogeneous container pattern)
제네릭은 Set<E>, Map<K, V> 등의 컬렉션과 ThreadLocal<T>, AtomicReference<T> 등의 단일원소 컨테이너도 흔히 쓰인다. 이런 모든 쓰임에서 매개변수화되는 대상은 원소가 아닌 컨테이너 자신이다. 따라서 하나의 컨테이너에서 매개변수화할 수 있는 타입의 수가 제한된다. 예를들어, Set에는 원소의 타입을 뜻하는 단 하나의 타입 매개변수만 있으면 되며, Map 에는 key, value의 타입을 뜻하는 2개만 필요한 식이다.
더 유연한 방식이 필요할때가 있다. 데이터베이스희 행(row)은 임의 개수의 열(column)을 가질 수 있는데, 모두 열을 타입 안전하게 이용할 수 있다면 더 편할 것이다. 여기에 쉬운 해법이 있다.
컨테이너 대신 키를 매개변수화한 다음, 컨테이너에 값을 넣거나 뺄때 매개변수화한 키를 함께 제공하면 된다.
이렇게 하면 제네릭 타입 시스템이 값의 타입이 키와 같음을 보장해 줄 것이다.
예시
타입별로 즐겨찾는 인스턴스를 저장하고 검색할 수 있는 Favorites 클래스를 생각해보자. 각 타입의 Class 객체를 매개변수화한 키 역할로 사용하면 된다. 이 방식이 동작하는 이유는 class의 클래스가 제네릭이기 때문이다. class 리터럴의 타입은 Class 가 아닌 Class<T>이다.
* 타입 토큰
컴파일타임 타입 정보과 런타임 타입 정보를 알아내기 위해 메서드를 주고받는 class 리터럴
Facorites.java API
public class Favorites {
public <T> void putFavorite(Class<T> type, T instance);
public <T> T getFavorite(Class<T> type);
}
Favorites.java
package com.java.effective.item33;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
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));
}
}
Favorites 호출 main 메서드
package com.java.effective.item33;
public class Main {
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);
// Java, cafebabe, class com.java.effective.item33.Favorites
System.out.printf("%s, %x, %s%n", favoriteString, favoriteInteger, favoriteClass);
}
}
Favorites 인스턴스는 타입 안전하다. String 을 요청했는데, Integer 을 반환할 일은 절대 없다. 또한 모든 키의 타입이 제각각이라, 일반적인 맵과 달리 여러가지 타입의 원소를 담을 수 있다. Favorites 는 타입 안전 이종(heterogeneous) 컨테이너라 할만하다.
private Map<Class<?>, Object> favorites = new HashMap<>();
Map<Class<?>, Object> 타입을 보고 '비한정적 와일드 카드 타입'이라 이 맵 안에 아무것도 넣을 수 없다고 생각하겠지만, 와일드카드 타입이 중첩되었다는 걸 알아야한다. 맵이 아니라 키가 와일드카드 타입인 것이다. 이는 모든 키가 서로 다른 매개변수화 타입일 수 있다는 뜻으로, 다양한 타입을 지원할 수 있다.
favorites 맵의 값 타입은 단순히 Object 이다. 이는 key, value 사이에 타입 관계를 보증하지 않는다는 것이다.
public <T> void putFavorite(Class<T> type, T instance) {
favorites.put(Objects.requireNonNull(type), instance);
}
주어진 Class 객체와 instance 를 favorites 에 각 key, value 로 추가했다. 해당 value가 그 키 type의 인스턴스라는 정보가 사라진다.
public <T> T getFavorite(Class<T> type) {
return type.cast(favorites.get(type));
}
주어진 Class 객체에 해당하는 값을 맵에서 꺼낸다. 이 객체가 바로 반환해야할 객체지만, 잘못된 컴파일 타임 타입을 가지고있다. 이 객체의 타입은 Object 이나, 우리는 이를 T로 바꿔서 반환해야한다. 따라서 getFavorite 구현은 Class 의 cast 메서드를 사용해 이 객체 참조를 Class 객체가 가리키는 타입으로 동적 형변환한다.
public class Class<T> {
T cast(Object obj);
}
cast 메서드의 반환 타입은 Class 객체의 타입 매개변수와 같다. 덕분에 T로 비검사 형변환하는 손실 없이도 Facorites 를 타입 안전하게 만들 수 있었다.
Facorites 클래스의 제약
1) 악의적인 클라이언트가 Class 객체를 제네릭이 아닌 로 타입으로 넘기면 타입안정성이 깨진다.
package com.java.effective.item33;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
public class Favorites {
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));
}
}
type.cast(instance) 를 사용하여 동적 형변환을 사용했다. 이는 type, instance 의 타입이 같은지를 확인해준다.
2) 실체화 불가 타입에는 사용할 수 없다.
String, String[]을 저장할수는 있어도, List<String>은 저장할 수 없다. 이는 컴파일 오류를 발생시킨다. List<String>용 Class 객체를 얻을 수 없기 때문이다. List<String>.class 는 오류가 발생한다. List<String> 과 List<Integer>은 List.class 라는 같은 Class 객체를 공유한다. 해당 제약의 해결방안은 없다.
한정적 타입 토큰
Favorites 클래스가 사용하는 타입 토큰은 비한정적이다. 타입 토큰을 다시 상기시켜보자.
* 타입 토큰
컴파일타임 타입 정보과 런타임 타입 정보를 알아내기 위해 메서드를 주고받는 class 리터럴
getFavorite, putFavorite 메서드는 어떤 Class 객체든 받아들이는데, 이 메서드들이 허용하는 타입을 제한하고 싶은 경우가 있다. 이때 한정적 타입 토큰을 사용하면 된다.
* 한정적 타입 토큰
단순히 한정적 타입 매개변수나 한정적 와일드카드를 사용하여 표현 가능한 타입을 제한하는 타입 토큰
public <T extends Annotation> T getAnnotation<Class<T> annotationType);
annotationType 인수는 애너테이션 타입을 뜻하는 한정적 타입 토큰이다. 이 메서드는 토큰으로 명시한 타입의 어노테이션이 대상 요소에 달려있다면 그 어노테이션을 반환하고, 없다면 null을 반환한다.
Class<?> 타입의 객체가 있고, 이를 한정적 타입 토큰을 받는 메서드에 넘기려면 어떻게 해야할까?
객체를 Class<? extends Annotation> 으로 형변환할 수도 있지만, 이 형변환은 비검사이므로 컴파일하면 경고가 뜰 것이다. 여기서 Class 클래스가 제공하는 asSubClass 메서드를 사용하여 호출된 인스턴스 자신의 class 객체를 인수가 명시한 클래스로 형변환할 수 있다. (이 클래스가 인수로 명시한 클래스의 하위 클래스라는 뜻이다.)
static Annotation getAnnotation(AnnotationElement 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));
}
asSubclass()
public <U> Class<? extends U> asSubclass(Class<U> clazz) {
if (clazz.isAssignableFrom(this))
return (Class<? extends U>) this;
else
throw new ClassCastException(this.toString());
}
위 코드는 오류나 경고없이 컴파일된다.
'Book > Effective Java' 카테고리의 다른 글
[교재 EffectiveJava] 아이템 35. ordinal 메서드 대신 인스턴스 필드를 사용하라 (0) | 2021.11.05 |
---|---|
[교재 EffectiveJava] 아이템 34. int 상수 대신 열거 타입을 사용하라 (0) | 2021.11.05 |
[교재 EffectiveJava] 아이템 32. 제네릭과 가변인수를 함께 쓸 때는 신중하라 (0) | 2021.11.03 |
[교재 EffectiveJava] 아이템 31. 한정적 와일드카드를 사용해 API 유연성을 높이라 (0) | 2021.11.02 |
[교재 EffectiveJava] 아이템 30. 이왕이면 제네릭 메서드로 만들라 (0) | 2021.11.01 |