제네릭은 Set<E> 등의 컬렉션과 ThreadLocal<T>, AtomicReference<T> 등의 단일원소 컨테이너에도 흔히 쓰인다.
이런 모든 쓰임에서 매개변수화되는 대상은 컨테이너 자신이다.
따라서 하나의 컨테이너에서 매개변수화할 수 있는 타입의 수가 제한된다.
하지만 더 유연한 수단이 필요할 때도 종종 있다.
데이터베이스의 행은 임의 개수의 열을 가질 수 있는데, 모두 열을 타입 안전하게 이용하려면?
컨테이너 대신 키를 매개변수화한 다음, 컨테이너에 값을 넣거나 뺄 때 매개변수화한 키를 함께 제공하면 된다.
이렇게 하면 제네릭 타입 시스템이 값의 타입이 키와 같음을 보장해줄 것이다.
이러한 설계 방식을 타입 안전 이종 컨테이너 패턴이라 한다.
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());
}
예제에선 key값으로 클래스 객체를 저장한다.
Favorites 인스턴스는 타입 안정하다. String을 요청했는데 Integer를 반환하는 일은 절대 없다.
또한 모든 키의 타입이 제각각이라, 일반적인 맵과 달리 여러 가지 타입의 원소를 담을 수 있다.
Favorites가 사용하는 private Map 변수는 비한정적 와일드카드 타입이다.
때문에 이 맵 안에 아무것도 넣을 수 없다고 생각할 수 있지만, 와일드카드가 중첩되었다는 점을 깨달아야 한다.
맵이 아닌 키가 와일드카드 타입이기 때문에 모든 키가 서로 다른 매개변수화 타입일 수 있고,
다양한 타입을 지원하는 힘이 여기서 나온다.
put은 Class 객체와 즐겨찾기 인스턴스를 favorites에 추가해 관계를 지으면 끝이다.
get은 Class의 cast 메서드를 이용해 동적 형변환을 해준다.
이 클래스에는 제약이 두가지 있다.
악의적인 클라이언트가 Class 객체를 로 타입으로 넘기면 해당 인스턴스의 타입 안전성이 쉽게 깨진다.
하지만 이렇게 짜여진 클라이언트 코드에서는 컴파일할 때 비검사 경고가 발생할 것이다.
HashSet과 HashMap 등의 일반 컬렉션 구현체에도 같은 문제가 있다.
그래서 타입 불변식을 어기는 일이 없도록 안전성을 보장하는 checkedSet 등과 같은 메서드가 있다.
다른 제약은 실체화 불가 타입에는 사용할 수 없다는 것이다.
다시 말해, 즐겨 찾는 String이나 String[]은 저장할 수 있어도 즐겨찾는 List<String>은 저장할 수 없다.
List<String> Class 객체를 얻을 수 없기 때문이다.
컬렉션 API로 대표되는 일반적인 제네릭 형태에서는 한 컨테이너가 다룰 수 있는 타입 매개변수의 수가 고정되어 있다.
하지만 컨테이너 자체가 아닌 키를 타입 매개변수로 바꾸면 이런 제약이 없는 타입 안전 이종 컨테이너를 만들 수 있다.
Class를 키로 사용하며, 이런 식으로 쓰이는 Class 객체를 타입 토큰이라 한다.
현재 구현되어 있는 라이브러리 중에서도 사용시 특정클래스.class 를 매개변수로 사용하는 것을 흔치 않게 볼 수 있는데,
이러한 패턴을 사용하여 유연성을 확보한 것이다.
질문 1: 타입 안전 이종 컨테이너란 무엇이고, 왜 고려해야 하는가?
타입 안전 이종 컨테이너란 서로 다른 타입의 객체를 담는 컨테이너를 말한다.
제네릭을 활용하여 다양한 타입을 안전하게 저장하고 검색할 수 있는 구조를 제공한다.
이중 컨테이너 방식을 활용하면 하나의 컨테이너에 다양한 타입을 저장하여 유연성을 보장할 수 있다.
또한 제네릭을 활용하여 컴파일 때 타입 체크가 발생해 타입 안전성을 확보할 수 있다.
질문 2: 타입 안전 이종 컨테이너를 구현할 때 고려해야 할 사항은 무엇인가?
1. 컨테이너의 타입 안정성을 보장하기 위해 제네릭 타입 매개변수를 사용해야 한다.
2. 타입 안전 이종 컨테이너를 구현할 때는 상속과 인터페이스를 적절히 활용해야 한다.
공통된 기능을 정의하는 인터페이스를 활용하거나, 상속을 통해 타입 관련 동작을 공유할 수 있다.
3. 컨테이너에 저장된 객체의 타입 정보를 유지해야 한다.
이를 위해 컨테이너에 타입 정보를 함께 저장하거나, 객체를 감싸는 래퍼 클래스를 활용할 수 있다.
'JAVA > 이펙티브 자바' 카테고리의 다른 글
35. ordinal 메서드 대신 인스턴스 필드를 사용하라 (0) | 2023.07.18 |
---|---|
34. int 상수 대신 열거 타입을 사용하라 (0) | 2023.07.18 |
32. 제네릭과 가변인수를 함께 쓸 때는 신중하라 (0) | 2023.07.16 |
31. 한정적 와일드카드를 사용해 API 유연성을 높이라 (0) | 2023.07.16 |
29,30. 이왕이면 제네릭 타입으로 만들라 (0) | 2023.07.16 |