> For the complete documentation index, see [llms.txt](https://incheol-jung.gitbook.io/docs/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://incheol-jung.gitbook.io/docs/study/effective-java/undefined-3/33.md).

# 아이템33 타입 안전 이종 컨테이너를 고려하라

제네릭은 `Set<E>`, `Map<K,V>` 등의 컬렉션과 `ThreadLocal<T>`, `AtomicReference<T>` 등의 단일 원소 컨테이너에도 흔히 쓰인다. 예컨대 `Set`에는 원소의 타입을 뜻하는 단 하나의 타입 매개변수만 있으면 되며, `Map`에는 키와 값의 타입을 뜻하는 2개만 필요한 식이다.

#### 더 유연한 수단이 필요할 때도 종종 있다.

데이터베이스의 `행(row)`은 임의 개수의 `열(column)`을 가질 수 있는데, 모두 열을 타입 안전하게 이용할 수 있다면 멋질 것이다.

#### 다행히 쉬운 해법이 있다.

`컨테이너` 대신 키를 매개변수화한 다음, 컨테이너에 값을 넣거나 뺄 때 `매개변수화`한 키를 함께 제공하면 된다. 이런 설계 방식을 타입 안전 `이종 컨테이너 패턴`이라 한다.

```java
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));
	}
}
```

```java
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` 클래스에는 알아두어야 할 제약이 두 가지 있다.

1. **악의적인 클라이언트가 Class 객체를 (제네릭이 아닌) `로 타입`(아이템 26)으로 넘기면 Favorite 인스턴스의 `타입 안전성`이 쉽게 깨진다.** 하지만 이는 클라이언트 코드에서 컴파일할 때 `비검사 경고`가 뜰 것이다.

   Favorites가 `타입 불변식`을 어기는 일이 없도록 보장하려면 `putFavorite` 메서드와 같이 `instance`의 타입이 `type`으로 명시한 타입과 같은지 확인하면 된다.

   `java.util.Collections`에는 `checkedSet`, `checkedList`, `checkedMap` 같은 메서드가 있는데 바로 이 방식을 적용한 컬렉션 래퍼들이다.
2. **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` 객체를 인수가 명시한 클래스로 형변환한다. (형변환된다는 것은 이 클래스가 인수로 명시한 클래스의 하위 클래스라는 뜻이다.)

```java
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` 객체를 타입 토큰이라 한다.
