일반적인 제네릭에서는 한 컨테이너가 다룰 수 있는 타입의 수가 정해져있다.
예를 들면, Map<K, V> 은 2개의 타입, Set<E> 은 1개의 타입을 갖는다.
하나의 컨테이너에서 다양한 타입을 다룰 수 없을까? 그래서 등장한 타입 안전 이종 컨테이너
어떻게 생겼나?
인터페이스
public interface Favorites {
<T> void putFavorite(Class<T> type, T instance);
<T> T getFavorite(Class<T> type);
}
구현체
public class MyFavorites implements Favorites {
private Map<Class<?>, Object> favorites = new HashMap<>();
@Override
public <T> void putFavorite(Class<T> type, T instance) {
favorites.put(Objects.requireNonNull(type), instance);
}
@Override
public <T> T getFavorite(Class<T> type) {
// type.cast()는 비검사 형변환 없이도 타입 안전하게 변환 한다.
return type.cast(favorites.get(type));
}
}
class를 키로 사용하며, 이를 타입 토큰이라 함
사용예
public class Main {
public static void main(String[] args) {
Favorites f = new MyFavorites();
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 org.effectivejava.item33.Favorites
타입 안전 이종 컨테이너의 단점은?
1. class 객체를 raw 타입으로 넘기면 타입의 안정성이 깨진다.
기존 MyFavorites 사용 시...
public class AttackRawClass {
public static void main(String[] args) {
/* raw 타입 class 공격 */
Favorites f = new MyFavorites();
// 타입의 안정성 깨짐
f.putFavorite((Class)Integer.class, "No Integer 인스턴스");
// 예외 발생!!!
int favoriteInteger = f.getFavorite(Integer.class);
System.out.printf("%d %n", favoriteInteger);
}
}
MyFavoritesDefenseRawClass 사용 시...
public class MyFavoritesDefenseRawClass implements Favorites {
private Map<Class<?>, Object> favorites = new HashMap<>();
@Override
public <T> void putFavorite(Class<T> type, T instance) {
/*
* raw type 공격 방어
* */
favorites.put(Objects.requireNonNull(type), type.cast(instance));
// favorites.put(Objects.requireNonNull(type), instance);
}
@Override
public <T> T getFavorite(Class<T> type) {
// type.cast()는 비검사 형변환 없이도 타입 안전하게 변환 한다.
return type.cast(favorites.get(type));
}
}
public class AttackRawClass {
public static void main(String[] args) {
/* raw 타입 class 방어 */
Favorites f = new MyFavoritesDefenseRawClass();
// 타입의 안정성 깨짐
// 예외 발생!!!
f.putFavorite((Class)Integer.class, "No Integer 인스턴스");
int favoriteInteger = f.getFavorite(Integer.class);
System.out.printf("%d %n", favoriteInteger);
}
}
checkedSet(), checkedList(), checkedMap() 컬렉션 래퍼들이 이와 같은 방법을 사용한다.
2. 실체화 불가 타입에는 사용할 수 없다.
String 이나 String[] 은 사용할 수 있어도 List<String> 이나 List<Integer> 는 저장할 수 없다.
List<String> 용 Class 객체를 얻을 수 없기 때문인데, 그 이유는 List<String> 이나 List<Integer> 나 List.class 라는 같은 Class 객체를 공유하기 때문이다.
확장편
지금까지의 Favorites 의 타입 토큰은 비한정적이다. 즉, getFavorite 과 putFavorite 은 어떤 Class 객체든 받아들인다.
이 메소드들의 타입을 제한하고 싶은 경우, <T extends MyToken> 를 사용하여 한정적 타입 토큰을 사용할 수 있다.
public class MyFavoritesLimitTypeToken {
private Map<Class<?>, Object> favorites = new HashMap<>();
public <T extends MyToken> void putFavorite(Class<T> type, T instance) {
favorites.put(Objects.requireNonNull(type), type.cast(instance));
}
public <T extends MyToken> T getFavorite(Class<T> type) {
return type.cast(favorites.get(type));
}
}
Class<?> 타입의 객체가 있고, 이를 한정적 타입 토큰을 받는 메소드에 넘기기 위해서는 객체를 Class<? extends MyToken> 으로 형변환할 수도 있지만, 이는 비검사 형변환 이라서 컴파일 경고가 발생한다.
이 때, 형변환을 안전하게 동적으로 수행해주는 asSubclass() 메소드를 사용하여 type.asSubclass(MyToken.class) 로 쓸 수 있다.
왜? 타입 안전 이종 컨테이너가 필요한가?
일반적인 제네릭에서는 한 컨테이너가 다룰 수 있는 타입의 수가 정해져있다. 예를 들면,
Map<K, V>
은 2개의 타입,Set<E>
은 1개의 타입을 갖는다.하나의 컨테이너에서 다양한 타입을 다룰 수 없을까? 그래서 등장한 타입 안전 이종 컨테이너
어떻게 생겼나?
인터페이스
구현체
사용예
Java cafebabe org.effectivejava.item33.Favorites
타입 안전 이종 컨테이너의 단점은?
1. class 객체를 raw 타입으로 넘기면 타입의 안정성이 깨진다.
기존
MyFavorites
사용 시...MyFavoritesDefenseRawClass
사용 시...checkedSet()
,checkedList()
,checkedMap()
컬렉션 래퍼들이 이와 같은 방법을 사용한다.2. 실체화 불가 타입에는 사용할 수 없다.
String
이나String[]
은 사용할 수 있어도List<String>
이나List<Integer>
는 저장할 수 없다.List<String>
용 Class 객체를 얻을 수 없기 때문인데, 그 이유는List<String>
이나List<Integer>
나List.class
라는 같은 Class 객체를 공유하기 때문이다.확장편
지금까지의
Favorites
의 타입 토큰은 비한정적이다. 즉,getFavorite
과putFavorite
은 어떤 Class 객체든 받아들인다. 이 메소드들의 타입을 제한하고 싶은 경우,<T extends MyToken>
를 사용하여 한정적 타입 토큰을 사용할 수 있다.Class<?>
타입의 객체가 있고, 이를 한정적 타입 토큰을 받는 메소드에 넘기기 위해서는 객체를Class<? extends MyToken>
으로 형변환할 수도 있지만, 이는 비검사 형변환 이라서 컴파일 경고가 발생한다. 이 때, 형변환을 안전하게 동적으로 수행해주는asSubclass()
메소드를 사용하여type.asSubclass(MyToken.class)
로 쓸 수 있다.