on
아이템33. 타입 안전 이종 컨테이너를 고려하라
아이템33. 타입 안전 이종 컨테이너를 고려하라
데이터베이스의 행과 열을 생각해보자.
행은 임의 개수의 열을 가질 수 있는데, 모두 열을 타입 안전하게 사용할 수 있으면 좋을 것 이다.
키를 매개변수화 한 다음, 컨테이너에 값을 넣거나 뺄 때 매개변수화한 키를 함께 제공하면
제네릭 타입 시스템이 값의 타입이 키와같음을 보장해줄 것 이다.
이런 설계 방식을 안전 이종 컨테이너 패턴 이라 한다.
타입 별로 즐겨찾는 인스턴스를 저장하고 검색하는 Favorite 클래스를 예시로 들어보자.
// 타입 안전 이종 컨테이터 패턴 - API class Favorites { public void putFavorite(Class type, T instance); public T getFavorite(Class type); }
각 타입의 Class 객체를 매개변수화한 키 역할로 사용하면 되는데, class의 클래스가 제네릭이기 때문이다.
String.class의 타입은 Class이고, Integer.class의 타입은 Class이다.
메서드들이 주고받는 class 리터럴을 타입 토큰 이라고 한다.
Favorite 클래스를 사용하는 예시를 보자.
// 타입 안전 이종 컨테이너 패턴 - 클라이언트 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); }
Favorite 인스턴스는 타입 안전하다. String 을 요청했는데 Integer을 반환하는 일은 절대 없다.
또한 모든 키의 타입이 달라 (일반적인 맵과 달리) 여러가지 타입의 원소를 담을 수 있다.
//타입 안전 이종 컨테이너 패턴 - 구현 public class Favorites { private Map, Object> favorites = new HashMap<>(); public void putFavorite(Class type, T instance) { favorites.put(Objects.requireNonNull(type), instance); } public T getFavorite type) { return type.cast
private Map 변수인 favorites의 타입은 Map, Object>이다.
맵이 아니라, 키가 와일드카드 타입이다. 다양한 타입을 지원할 수 있게 하는 이유이다.
다음으로 favorites 맵의 값 타입은 단순히 Object이다. 이 맵은 키와 값 사이의 타입 관계를 보증하지 않는다.
하지만 getFavorite 메서드에서 이 관계를 되살릴 수 있다.
getFavorite은 먼저 주어진 type에 해당하는 값을 favorites 맵에서 꺼낸다.
꺼낸 객체는 Object이니 우리는 이를 T로 바꿔 리턴해야 한다.
이를 위해 Class의 cast 메서드를 사용해 객체를 Class 객체가 가르키는 타입으로 동적 형변환 한다.
cast 메서드는 형변환 연산자의 동적 버전이다.
이 메서드는 Class 객체가 알려주는 타입의 인스턴스인지를 검사한 다음,
맞다면 해당 타입으로 인수를 반환하고
아니면 ClassCastException을 던진다.
public class Class { T cast (Object obj); }
cast 메서드는 Class 클래스가 제네릭이라는 이점을 활용하여 T로 비검사 형변환하지 않고 Favorites를 타입 안전하게 만들어준다.
지금까지 만들어본 Favorites 클래스에는 알아두어야 할 제약이 두가지 있다.
1. 악의적인 클라이언트가 Class객체를 (제네릭이 아닌) 로타입으로 넘기면 인스턴스의 타입 안전성이 쉽게 깨진다.
하지만 클라이언트 코드에서는 컴파일할 때 비검사 경고가 뜰 것이다.
Favorites가 타입 불변식을 어디는 일이 없도록 보장하려면 putFavorite 메서드에서 동적 형변환을 이용해 확인하면 된다.
// 동적 형변환을 런타임 타입 안전성 확보 public void putFavorite(Class type, T instance) { favorites.put(Objects.requireNonNull(type), type.cast(Instance)); }
2. 실체화 불가 타입에는 사용할 수 없다. (아이템28)
Favorites의 타입으로 String이나 String[]은 사용할 수 있어도, List은 사용할 수 없다는 것이다.
List용 Class객체가 없기 때문이다.
List과 List은 List.class라는 같은 Class객체를 공유한다.
위에서 만든 Favorites가 사용하는 타입 토큰은 비한정적이다. 즉 getFavorite과 putFavorite은 모든 Class객체를 받는다.
때로는 메서드들이 허용하는 타입을 제한하고 싶을 수 있는데, 한정적 타입 토큰을 사용하면 가능하다.
한정적 타입 토큰은 한정적 타입 매개변수나 한정적 와일드카드를 사용하는 타입 토큰 이다.
애너테이션 API는 한정적 타입 토큰을 적극적으로 사용한다.
// 애너테이션 - API public T getAnnotation(Class annotationType);
Class 타입의 객체를 한정적 타입 토큰을 받는 메서드로 넘기려면 어떻게 해야할까?
객체를 Class으로 형변환 하면 비검사 경고가 뜰 것 이다.
Class 클래스에서 이런 형변환을 동적으로 안전하게 수행하는 asSubclass 메서드를 제공한다.
asSubClass는 호출된 인스턴스 자신의 Class객체를 인수가 명시한 클래스로 형변환 한다.
(형변환이 된다면, 이 클래스가 인수로 명시한 클래스의 하위클래스라는 뜻이다.)
성공하면 인수로 받은 클래스 객체를 반환하고, 실패하면 ClassCastExcption을 던진다.
//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)); }
from http://makemoneyamjay.tistory.com/18 by ccl(A) rewrite - 2021-09-25 12:01:14