[Why]
제네릭을 쓸 때 매개변수화되는 대상은 원소가 아닌 컨테이너 자신이다.
- 따라서 하나의 컨테이너에서 매개변수화할 수 있는 타입의 수가 제한된다.
- 예컨대, Set에는 원소의 타입을 뜻하는 단 하나의 타입 매개변수만 있으면 되며, Map에는 키와 값을 뜻하는 2개만 필요한 식이다.
하지만 더 유연한 수단이 필요할 때도 종종 있다.
- 예컨대 데이터베이스의 행(row)은 임의 개수의 열(column)을 가질 수 있는데, 모든 열을 타입 안전하게 이용한다면 좋을 것이다.
Map<KeyType, ValueType> map1 = new HashMap<>();
// ValueType이 한번 고정되면 ValueType만 넣어야 함
Map<KeyType, Object> map2 = new HashMap<>();
// Object에는 아무거나 들어갈 수 있으나 타입 안전하지 않음.
- map2를 타입 안전하게 쓰고 싶다면?
[What]
타입 안전 이종 컨테이너 패턴이란?
컨테이너 대신 키를 매개변수화한 다음, 컨테이너에 값을 넣거나 뺄 때 매개변수화한 키를 함께 제공하면 된다.
- 이렇게 하면 제네릭 타입 시스템이 값의 타입이 키와 같음을 보장해줄 것이다.
- 이러한 설계 방식을 타입 안전 이종 컨테이너 패턴(type safe heterogeneous container pattern)이라 한다.
컨테이너 안에 서로 다른 종(type)들이 들어있는데 타입 안전하게 쓸 수 있다는 뜻.
// 타입 안전 이종 컨테이너 패턴 - API
public class Favorites {
public <T> void putFavorite(Class<T> type, T instance);
public <T> T getFavorite(Class<T> type);
}
- 위 코드는 타입별로 즐겨 찾는 인스턴스를 저장하고 검색할 수 있는 Favorites 클래스이다.
- 각 타입의 Class 객체를 매개변수화한 키 역할로 사용하는데, 이 방식이 동작하는 이유는 class의 클래스가 제네릭이기 때문이다.
- class 리터럴의 타입은 Class가 아닌 Class<T>다.
e.g.) String.class의 타입은 Class<String>, Integer.class의 타입은 Class<Integer>이다. - 컴파일타임 타입 정보와 런타임 타입 정보를 알아내기 위해 메서드들이 주고받는 class 리터럴을 타입 토큰(type token)이라 한다.
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 출력
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)); // 런타임에는 Object로 들어가있으므로 동적 형변환
}
}
// public T cast(Object obj) {
// if (obj != null && !isInstance(obj))
// throw new ClassCastException(cannotCastMsg(obj));
// return (T) obj;
// }
Favorites가 사용하는 private 맵 변수인 favorites의 타입은 Map<Class<?>, Object>이다.
- 비한정적 와일드카드 타입이라 이 맵 안에 아무것도 넣을 수 없다고 생각할 수 있지만, 사실은 그 반대다.
- 와일드카드 타입이 중첩(nested)되었다는 점을 깨달아야 한다.
- 맵이 아니라 키가 와일드카드 타입인 것이다.
- 이는 모든 키가 서로 다른 매개변수화 타입일 수 있다는 뜻으로, 첫번째는 Class<String>, 두번째는 Class<Integer>일 수 있다.
그 다음으로 알아둘 점은 favorites 맵의 값 타입은 단순히 Object라는 것이다.
- 이 맵은 키와 값 사이의 타입 관계를 보증하지 않는다는 말이다.
- 자바의 타입 시스템에서는 이 관계를 명시할 방법이 없다.
키와 값 사이의 '타입 링크(type linkage)' 정보가 버려지기 때문이다. - 하지만 우리는 이 관계가 성립함을 알고 있고, 즐겨찾기를 검색할 때 그 이점을 누리게 된다.
cast 메서드는 형변환 연산자의 동적 버전이다.
- 이 메서드는 단순히 주어진 인수가 Class 객체가 알려주는 타입의 인스턴스인지를 검사한 다음, 맞다면 그 인수를 그대로 반환하고, 아니면 ClassCastException을 던진다.
- 클라이언트 코드가 깔끔히 컴파일된다면, favorites 맵 안의 값은 해당 키의 타입과 항상 일치함을 알고 있다.
- 그런데 cast 메서드를 굳이 왜 사용하는 것일까?
public class Class<T> {
T cast(Object obj);
}
- 위 코드에서 보듯 cast의 반환 타입은 Class 객체의 타입 매개변수와 같다.
- 이것이 T로 비검사 형변환하는 손실 없이도 Favorites를 타입 안전하게 만드는 비결이다.
return (T) favorites.get(type); 하지 않고 매개변수화된 타입으로 반환할 수 있음.
[How]
한정적 타입 토큰
Favorites가 사용하는 타입 토큰은 비한정적이다. 때로는 이 메서드들이 허용하는 타입을 제한하고 싶을 수 있는데, 한정적 타입 토큰을 활용하면 가능하다.
- 한정적 타입 토큰이란 단순히 한정적 타입 매개변수(아이템29)나 한정적 와일드카드(아이템31)를 사용하여 표현 가능한 타입을 제한하는 타입 토큰이다.
- 애너테이션 API(아이템39)는 한정적 타입 토큰을 적극적으로 사용한다.
public <T extends Annotation> T getAnnotation(Class<T> annotationType);
- 위 메서드는 AnnotatedElement 인터페이스에 선언된 메서드로, 대상 요소에 달려 있는 애너테이션을 런타임에 읽어온다.
Class<?> 타입의 객체가 있고, 이를 한정적 타입 토큰을 받는 메서드에 넘기려면 어떻게 해야 할까?
- Class<? extends Annotation>으로 형변환할 수도 있지만, 이 형변환은 비검사이므로 컴파일하면 경고가 뜰 것이다(아이템27).
- 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));
}
제약 두 가지
첫번째, 악의적인 클라이언트가 Class 객체를 로 타입으로 넘기면 Favorites 인스턴스의 타입 안전성이 쉽게 깨진다.
f.putFavorite((Class)Integer.class, "Integer의 인스턴스가 아닙니다.");
int favoriteInteger = f.getFavorite(Integer.class);
- putFavorite을 호출할 때는 아무 불평이 없다가 getFavorite을 호출할 때 ClassCastException을 던진다.
- HashSet, HashMap 등의 일반 컬렉션 구현체에도 똑같은 문제가 있다.
// 다음 코드가 컴파일도 되고 동작도 한다.
HashSet<Integer> set = new HashSet<>();
((HashSet)set).add("문자열입니다.");
- Favorites가 타입 불변식을 어기는 일이 없도록 보장하려면 putFavorite 메서드에서 인수로 주어진 instance의 타입이 type으로 명시한 타입과 같은지 확인하면 된다.
public <T> void putFavorite(Class<T> type, T instance) {
favorite.put(Objects.requireNonNull(type), type.cast(instance));
}
- java.util.Collections의 checkedSet, checkedList, checkedMap 같은 메서드들이 이 방식을 적용한 컬렉션 래퍼들이다.
- 이 래퍼들은 내부 컬렉션들을 실체화한다.
런타임에 Coin을 Collection<Stamp>에 넣으려하면 ClassCastException을 던진다.
두번째, 실체화 불가 타입(아이템28)에는 사용할 수 없다.
- 즐겨 찾는 String이나 String[]은 저장할 수 있어도 즐겨 찾는 List<String>은 저장할 수 없다.
- List<String>용 Class 객체를 얻을 수 없기 때문에 컴파일되지 않는다.
제네릭(<String>)은 런타임에 소거되기 때문이다.
슈퍼 타입 토큰(super type token)으로 해결할 수 있지만 완벽하지는 않다.
'독서찰기(讀書札記) > 이펙티브 자바' 카테고리의 다른 글
[아이템 35] ordinal 메서드 대신 인스턴스 필드를 사용하라 (0) | 2022.02.20 |
---|---|
[아이템 34] int 상수 대신 열거 타입을 사용하라 (0) | 2022.02.18 |
[아이템 32] 제네릭과 가변인수를 함께 쓸 때는 신중하라 (0) | 2022.02.15 |
[아이템 31] 한정적 와일드카드를 사용해 API 유연성을 높이라 (0) | 2022.02.13 |
[아이템 30] 이왕이면 제네릭 메서드로 만들라 (0) | 2022.02.12 |