타입안전이종..컨..머시기..ㅇ_ㅇ
제네릭은 Set<E>, Map<K,V> 등의 컬렉션과 ThreadLocal<T>, AtomicReference<T> 등의 단일원소 컨테이너에도 흔히 쓰인다고 한다.
위와 같은 경우 매개변수화 되는 대상은 원소가 아닌 컨테이너 자신이다.
-> 따라서 하나의 컨테이너에서 매개변수화할 수 있는 타입의 수가 제한된다.
이는 컨테이너의 일반적인 용도에 맞게 설계된 것이다.
컨테이너란, 대충 큰 범주로 생각해봤을 때 값을 저장할 수 있는 객체를 의미하는 것이므로..
그런데?
일반적으로 제네릭에서 타입의 수가 제한된다. -> 이 부분이 문제가 되는 것이다.
예를 들어 위 예시의 Map<k,v>과 같은 경우, V타입을 통해서만 벨류를 저장할 수 있다.
따라서 파라미터화 된 제네릭 타입 Map<String, Double>이 있다면? Double 이외의 값은 담을 수가 없다.
그런데 내가 이 Map에 Double이 아니라 String, Boolean을 담고 싶고, 얘를 모두 타입 안정하게 이용하고 싶을 수도 있다.
이런 경우에 서로 다른 타입을 하나의 컨테이너에 안전하게 보관할 수 있게 하는 것이 " 타입 안전 이종 컨테이너 " 이다.
타입 안전 이종 컨테이너 패턴(type safe heterogeneous container pattern)
- 컨테이너 대신 키를 매개변수화한 다음, 컨테이너에 값을 넣거나 뺄 때 매개변수화한 키를 함께 제공한다.
Map<String, Integer> : 컨테이너 매개변수화
Map<Class<?>, Object> : 컨테이너 키 매개변수화
-> 제네릭 타입 시스템이 값의 타입과 키가 같음을 보장해준다.
class 리터럴의 타입 : Class<T> (Class 아님)
ex. String.class -> Class<String>, Integer.class -> Class<Integer>
컴파일타임 타입 정보와 런타임 타입 정보를 알아내기 위하여
메서드들이 주고받는 class 리터럴을 타입 토큰(type token)이라 한다.
예시를 통해 알아봅시다..
"타입 안전 이종 컨테이너 패턴 - API"
public class Favorites {
public <T> void putFavorite(Class<T> type, T instance) {
// ...
}
public <T> T getInstance(Class<T> type) {
return null; // 편의상 null을 리턴하도록 작성했다.
}
}
- 위 Favorites 클래스는 키가 매개변수화 되었다.
- 클라이언트는 즐겨찾기를 저장하거나 얻어올 때 Class 객체를 알려주면 된다.
"타입 안전 이종 컨테이너 패턴 - 클라이언트"
public class Favorites {
public static void main(String[] args) {
Favorites favorites = new Favorites();
favorites.putFavorite(String.class, "Java");
favorites.putFavorite(Integer.class, 0xcafebabe);
favorites.putFavorite(Class.class, Favorites.class);
String favoriteString = favorites.getInstance(String.class);
int favoriteInteger = favorites.getInstance(Integer.class);
Class<?> favoriteClass = favorites.getInstance(Class.class);
System.out.printf("%s %x %s%n", favoriteString, favoriteInteger, favoriteClass.getName());
}
}
- Favorites 인스턴스는 타입 안전하다. 즉 String 요청왔다고 가정 한다면, 다른 타입을 반환할 일이 없음.
- 또한 일반 맵과 다르게 여러 타입의 원소를 담을 수 있다.
- 모든 키의 타입이 제각각이기 때문이다.
- -> Favorites는 타입 안전 이종 컨테이너!
" 타입별로 즐겨 찾는 인스턴스를 저장하고 검색할 수 있는 Favorites 클래스 "
private Map<Class<?>, Object> favorites = new HashMap<>();
public <T> void putFavorites(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의 타입은 Map<class<?>, Object>이다. (비한정적 와일드카드 타입)
- 비한정적 와일드카드 타입이라 이 맵 안에 아무것도 넣을 수 없는 것이 아니다. -> 와일드카드가 중첩(nested)되어 있기 때문에 가능하다.
- 맵이 아니라 키가 와일드카드 타입인 것이다. 이는 모든 키가 서로 다른 매개변수화 타입일 수 있다는 뜻이다. 이를 통해 다양한 타입을 지원할 수 있다.
- favorites 맵의 값 타입은 단순히 Object다. 이 맵은 키와 값 사이의 타입 관계를 보증하지 않는다는 뜻이다.
- 즉, 모든 값이 키로 명시한 타입임을 보증하지 않는다.
- 자바의 타입 시스템에서 이 관계를 명시할 방법도 없다.
- putFavorite의 구현
- 주어진 Class 객체와 즐겨찾기 인스턴스를 favorites에 추가해 관계를 지으면 끝
- 키와 값 사이의 ‘타입 링크(type linkage)’ 정보는 버려짐
- 즉, 그 값이 그 키 타입의 인스턴스라는 정보가 사라진다.
- 하지만 getFavorite 메서드에서 이 관계를 되살릴 수 있으니 상관없다.
- getFavorite의 구현
- 주어진 Class 객체에 해당하는 값을 favorites 맵에서 꺼낸다.
- 이 객체의 타입은 (favorites 맵의 값 타입인)Object이나, 우리는 이를 T로 바꿔 반환해야 한다.
- 이 객체가 바로 반환해야 할 객체가 맞지만, 잘못된 컴파일타임 타입을 가지고 있으므로
- Class와 cast 메서드를 사용해 이 객체 참조를 Class 객체가 가리키는 타입으로 동적 형변환한다.
- cast method??
- cast 메서드는 형변환 연산자의 동적 버전
- 단순히 주어진 인수가 Class 객체가 알려주는 타입의 인스턴스인지를 검사한 다음, 맞다면 그 인수를 그대로 반환하고, 아니면 ClassCaastException을 던진다.
- favorites 맵 안의 값은 해당 키의 타입과 항상 일치하기 때문에 클라이언트 코드가 항상 깔끔히 컴파일됨을 알 수 있다. 따라서 cast메서드가 ClassCaastException 예외를 던질 일은 없다.
- 그럼 그냥 인수만 반환하는데.. 왜 얘를 사용할까?
- cast 메서드의 시그니처가 Class 클래스가 제네릭이라는 이점을 활용하고 있기 때문.
- cast의 반환 타입은 Class 객체의 타입 매개변수와 같다.
- 이것이 바로 getFavorite 메서드에 필요한 기능 (T로 비검사 형변환하는 손실 없이도 Favorites를 타입 안전하게 만든다.)
- 그럼 그냥 인수만 반환하는데.. 왜 얘를 사용할까?
- cast method??
열심히 만든 위 클래스에서 꼭 알아두어야 할 제약 2가지
1. 악의적인 클라이언트가 Class 객체를 (제네릭이 아닌) 로 타입으로 넘기면 Favorites 인스턴스의 타입 안전성이 쉽게 깨진다.
(비검사 경고가 뜨긴 한다.)
public <T> void putFavorites(Class<T> type, T instance) {
favorites.put(Objects.requireNonNull(type), type.cast(instance));
}
"동적 형변환을 시켜줘서 런타임 타입 안정성을 확보하면 된다."
2. 실체화 불가 타입에는 사용할 수 없다.
즉 String이나 String[]은 저장할 수 있어도 즐겨 찾는 List<String>은 저장할 수 없다.
이 제약에서 만족스러운 우회로는 없다.
- 슈퍼 타입 토큰이라는 애로 해결하려는 시도가 있긴 하다고 한다. 머리가 아프므로 그냥 넘어가자.
Favorites가 사용하는 getFavorite과 putFavorite은 어떤 Class 객체든 받아들인다.
즉, 타입 토큰이 비한정적이다.
이러한 타입을 제한하고 싶은 경우가 있을텐데, 이럴 때는 한정적 타입 토큰을 활용한다.
한정적 타입 토큰?
- 단순히 한정적 타입 매개변수나 한정적 와일드카드를 사용하여 표현 가능한 타입을 제한하는 타입 토큰
핵심 정리
컬렉션 API로 대표되는 일반적인 제네릭 형태에서는 한 컨테이너가 다룰 수 있는 타입 매개변수의 수가 고정되어 있다.
컨테이너 자체가 아닌 키를 타입 매개변수로 바꾸면 이런 제약이 없는 타입 안전 이종 컨테이너를 만들 수 있다.
타입 안전 이종 컨테이너는 Class를 키로 쓰며, 직접 구현한 키 타입도 쓸 수 있다.
이런 식으로 쓰이는 Class 객체를 타입 토큰이라 한다.
ex) 데이터베이스의 행(컨테이너)를 표현한 DatabaseRow 타입 은 제네릭 타입은 Column<T>를 키로 사용한다.
'JAVA > Effective Java' 카테고리의 다른 글
item 35. ordinal 메서드 대신 인스턴스 필드를 사용하라 (0) | 2022.05.31 |
---|---|
item 34. 상수 대신 열거 타입을 사용하라 (0) | 2022.05.31 |
item 31. 한정적 와일드카드를 사용해 API 유연성을 높여라. (0) | 2022.05.17 |
item 30. 이왕이면 제네릭 메서드로 만들어라 (0) | 2022.05.14 |
item 29. 이왕이면 제네릭 타입으로 만들어라 (0) | 2022.05.13 |
댓글