반응형
이 포스팅에서 작성하는 내용은 EFFECTIVE JAVA(이펙티브자바) 에서 발췌하였습니다.
아이템 33. 타입 안전 이종 컨테이너를 고려하라
제네릭
- Set<E>, Map<K, V> 등의 컬렉션과 ThreadLocal<T>, AtomicReference<T> 등의 단일원소 컨테이너에도 흔히 쓰인다.
- 이런 모든 쓰임에서 매개변수화되는 대상은 원소가 아닌 컨테이너 자신이다며 하나의 컨테이너에서 매개변수화 할 수 있는 타입의 수가 제한된다.
- 예컨데 Set에는 원소의 타입을 뜻하는 단 하나의 타입 매개변수만 있으면 되며, Map에는 키와 값의 타입을 뜻하는 2개만 필요한 식이다.
타입 안전 이종 컨테이너
- 컨테이너 대신 키를 매개변수화한 다음, 컨테이너에 값을 넣거나 뺄 때 매개변수화한 키를 함께 제공하면 된다.
- 제네릭 타입 시스템이 값의 타입이 키와 같음을 보장하며, 이러한 설계 방식을 타입 안전 이종 컨테이너 패턴이라 한다.
ex 1) 타입 안전 이종 컨테이너 패턴 예시
public class Favorites {
public <T> void putFavorite(Class<T> type, T instance);
public <T> T getFavorite(Class<T> type);
}
public static void main(String[] args) {
Favorites favorites = new Favorites();
favorites.putFavorite(String.class,"effectiveJava");
favorites.putFavorite(Integer.class, 0xcafebabe);
favorites.putFavorite(Class.class, Favorites.class);
String favoriteString = favorites.getFavorite(String.class);
Integer favoriteInteger = favorites.getFavorite(Integer.class);
Class<?> favoriteClass = favorites.getFavorite(Class.class);
// 출력 : Java cafebabe Favorites
System.out.printf("%s %x %s", favoriteString, favoriteInteger, favoriteClass.getName());
}
- 각 타입의 Class 객체를 매개변수화한 키 역할로 사용하면 되는데, 이 방식이 동작하는 이유는 class의 클래스가 제네릭이기 때문
- class의 리터럴 타입은 Class가 아닌 Class<T> ex) String.class → Class<String>, Integer.class → Class<Integer>
- 컴파일타임 타입 정보와 런타임 타입 정보를 알아내기 위해 메서드들이 주고받는 class 리터럴을 타입 토큰이라 함
- Favorites 인스턴스는 타입 안전함
- 모든 키의 타입이 제각각이라 일반적인 Map 과 달리 여러 타입의 원소를 담을 수 있어 타입 안전 이종 컨테이너라고 볼 수 있다.
ex2) 타입 안전 이종 컨테이너 패턴의 구현 (ex 1의 구현)
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 맵의 값 타입은 단순히 Object라는 것(키와 값 사이의 타입 관계를 보증하지 않는다는 것) 즉, 모든 값이 키로 명시한 타입임을 보증하지 않는다.
- putFavorite 구현은 주어진 Class 객체와 즐겨찾기 인스턴스를 favorites에 추가해 관계를 지으면 끝
- 즉, 그 값이 그 키 타입의 인스턴스라는 정보가 사라짐
- 하지만, getFavorite 메서드에서 이 관계를 되살릴 수 있으니 상관없음
- getFavorite 는 주어진 Class 객체에 해당하는 값을 favorites 맵에서 꺼냄. 이 객체가 반환해야할 객체지만 Object 타입을 가지고 있음 → 이를 T로 바꿔서 반환하면 된다.(cast 메서드 이용)
- cast 메서드가 단지 인수를 그대로 반환하기만 한다면 왜 사용할까?
- cast 메서드의 시그니처가 Class 클래스가 제네릭이라는 이점을 완벽히 활용하여 타입 안전하게 만듬
public class Class<T> {
T cast(Object object);
}
위의 Favorite 클래스의 주의점
1) 악의적인 클라이언트가 Class 객체를 로 타입으로 넘기면 Favorites 인스턴스의 타입 안정성이 쉽게 깨짐
ex 3) 동적 형변환으로 런타임 안정성 확보
public <T> void putFavorite(Class<T> type, T instance){
favorites.put(Objects.requireNonNull(type), type.cast(instance));
}
- Favorites가 타입 불변식을 어기는 일이 없도록 보장하려면 putFavorite 메서드에서 인수로 주어진 instance의 타입이 type으로 명시한 타입과 같은 지 확인
- java.utill.Collections의 checkedSet, checkedList, checkedMap 같은 메서드는 이러한 방식을 적용한 컬렉션 래퍼들
- 이 래퍼들은 제네릭과 로 타입을 섞어 사용하는 애플리케이션에서 클라이언트 코드가 컬렉션에 잘못된 타입의 원소를 넣지 못하게 추적하는 데 도와준다.
2) 실체화 불가 타입에는 사용 불가
- String이나 String[]은 저장할 수 있어도 List<String>은 저장할 수 없다
- 이 제약을 슈퍼 타입 토큰이라는 방식으로 해결할 수는 있지만, 완벽하지 않으므로 주의해서 사용해야 한다.
한정적 타입 토큰
- 위의 Favorites가 사용하는 타입 토큰은 비한정적이다.
- 하지만, 한정적 타입 매개변수나 한정적 와일드카드를 사용해서 허용하는 타입을 제한할 수 있다.
ex4) AnnotatedElement 인터페이스에 선언된 메서드
public <T extends Annotation> T getAnnotation(Class<T> annotationType);
- 대상 요소에 달려 있는 어노테이션을 런타임에 읽어오는 기능을 하는 메서드
- 이 메서드는 리플렉션의 대상이 되는 타입들. 즉, 클래스, 메서드, 필드 같이 프로그램 요소를 표현하는 타입들에서 구현
- 여기서 annotationType 인수는 어노테이션 타입을 뜻하는 한정적 타입 토큰이다.
- 이 메서드는 토큰으로 명시한 타입의 어노테이션이 대상 요소에 달려있다면 그 어노테이션을 반환하고, 없다면 null을 반환
- 즉, 어노테이션된 요소는 그 키가 어노테이션 타입인, 타입 안정 이종 컨테이너
ex 5) asSubclass를 사용해 한정적 타입 토큰을 안전하게 형변환하는 예
static Annotation getAnnotation(AnnotatedElement element, String annotationTypeName){
Class<?> annotationType = null; // 비한정적 타입 토큰
try {
annotationType = Class.forName(annotationTypeName);
}catch (Exception e){
throw new IllegalArgumentException(e);
}
return element.getAnnotation(annotationType.asSubclass(Annotation.class));
}
- asSubclass 메서드는 호출된 인스턴스 자신의 Class 객체를 인수가 명시한 클래스로 형변환한다. (형변환된다는 건, 이 클래스가 인수로 명시한 클래스의 하위 클래스라는 뜻)
- 형변환에 성공하면 인수로 받은 클래스 객체를 반환하고 실패 시, ClassCastException을 던진다.
정리
- 컬렉션 API로 대표되는 일반적인 제네릭 형태에서는 한 컨테이너가 다룰 수 있는 타입 매개변수의 수가 고정되어 있다.
- 컨테이너 자체가 아닌 키를 타입 매개변수로 바꾸면 이런 제약이 없는 타입 안전 이종 컨테이너로 만들 수 있다.
- 타입 안전 이종 컨테이너는 Class를 키로 쓰며 이런 식으로 쓰이는 Class 객체를 타입 토큰이라 하며, 직접 구현한 키 타입도 쓸 수 있다.
반응형
'BE > Java' 카테고리의 다른 글
[Effective Java] 35. ordinal 메서드 대신 인스턴스 필드를 사용하라 (0) | 2022.10.24 |
---|---|
[Effective Java] 34. int 상수 대신 열거 타입을 사용하라 (0) | 2022.10.21 |
[Effective Java] 아이템 32. 제네릭과 가변인수를 함께 쓸 때는 신중하라 (1) | 2022.10.18 |
[Effective Java] 아이템 31. 한정적 와일드카드를 사용해 API 유연성을 높이라 (1) | 2022.10.18 |
[Effective Java] 아이템 30. 이왕이면 제네릭 메서드로 만들라 (1) | 2022.10.13 |