- Type Safe Heterogeneous Container ⇒ 타입 안전한 여러가지를 담을 수 있는 컨테이너
- 여기서 컨테이너란 List, Set, Map 같이 여러 요소를 담을 수 있는 클래스를 의미함.
- 기존의 컨테이너에서 사용되는 제네릭은 사용할 수 있는 개수가 한정됨. 예컨데, List 는
List<String>
와 같이 하나의 타입만을, Map 은Map<String, Integer>
같이 한번에 두개의 타입만을 사용할 수 있다.- 제네릭 타입이 정해졌을 때, 컨테이너에는 해당 타입의 요소만 관리할 수 있게됨.
- 타입을 조금 더 유연하게 여러개를 사용하고 싶을 때, 타입 안전 이종 컨테이너를 사용한다.
가능 하긴 함. 하지만, 컨테이너로부터 얻은 객체의 타입을 정확히 알기 힘듦. 컴파일 에러는 없을지라도 런타임 시 에러가 발생하지 않는다는 보장은 없음.
Set<Object> everything = new HashSet<>();
everything.add("Hello");
everything.add(123);
everything.add(1234L);
everything.add(1.00);
everything.forEach(System.out::println);
// 에러 없이 잘 작동함
Map<Class<?>, Object> map = new HashMap<>();
map.put(Integer.class, 13);
map.put(String.class, "hello");
System.out.println((int) map.get(Integer.class)); // 13
Integer.class 의 타입은 Class, String.class 의 타입은 Class 이다.
위 처럼 코드를 작성하게 되면, Key 에는 타입이 Value 에는 값이 들어가게 되어 값의 타입을 추측할 수 있게 된다. 하지만 아래처럼 누군가 키와 밸류를 서로 다른 타입으로 집어 넣는다면?
map.put(Integer.class, "I am not a number");
System.out.println((int) map.get(Integer.class));
// 런타임 에러 : ClassCastException
컴파일 타임엔 알 수 없는 런타임 에러가 발생한다. Value 의 타입이 Object
이기 때문에 타입 캐스팅을 해줘야하기 때문.
public class Box {
private Map<Class<?>, Object> favorites = new HashMap<>();
public <T> void put(Class<T> type, T instance) {
favorites.put(Objects.requireNonNull(type), instance);
}
public <T> T get(Class<T> type) {
return type.cast(favorites.get(type));
}
}
위 코드가 타입 안전 이종 컨테이너의 구현이다.
// Box.java
public <T> void put(Class<T> type, T instance) {
favorites.put(Objects.requireNonNull(type), instance);
}
// Application.java
Box box = new Box();
box.put(Integer.class, 123);
box.put(Integer.class, "abcd"); // 컴파일 에러
Objects.requireNonNull 은 매개변수가 null 일경우 NullPointerException 을 발생시킨다. 아니라면, 매개변수를 그대로 반환한다.
아까 언급했듯 Integer.class
의 타입은 Class<Integer>
이고, String.class
의 타입은 Class<String>
이라고 했다. 따라서 위 코드와 같이 put
을 호출하게되면, 타입 추론을 통해 T
는 Integer
가 된다.
따라서 마지막 줄은 타입이 다르므로 (T는 Integer 인데, 매개변수가 String) 컴파일 에러가 발생한다.
// Box.java
public <T> T get(Class<T> type) {
return type.cast(favorites.get(type));
}
// Application.java
System.out.println(
box.get(Integer.class)
);
get
메소드에 type.cast
는 동적으로 타입 캐스팅을 해주는 메소드이다. 현재 매개변수로 Integer.class
를 받아오므로, get
메소드 내부에서 자동으로 Integer
로 타입캐스팅을 해준다. 따라서 외부에서 별도로 타입 캐스팅을 해줄 필요가 없다.
box.put((Class) Integer.class, "abc 되지롱");
위와 같이 Integer.class
를 (Class)
로 캐스팅해 Class<Integer>
가 아닌 Class
인 Raw 타입으로 전달하게 되면, 타입 안정성이 깨지게된다. 위 코드는 잘못된 타입이 전달되었지만 컴파일 타임에도, 런타임에도 오류가 발생하지 않는다.
이를 막기 위해 put
메소드에서도 형변환 검사를 추가해줘야 한다.
public <T> void put(Class<T> type, T instance) {
favorites.put(Objects.requireNonNull(type), type.cast(instance));
// instance 도 type.cast 를 추가해줌.
}
(관련 아이템: 28. 배열보다는 리스트를 사용하라)
String
과 String[]
은 가능하지만, List<String>
용 Class 객체는 얻을 수 없다. 따라서 List<String>
, Set<Integer>
등은 컴파일 할 수 없다.
box.put(Set<Integer>.class, Set.of(1, 2, 3, 4)); // 컴파일 불가능
컨테이너에 값을 넣거나 뺄 때 매개변수화한 키를 함께 제공하여 제너릭 타입 시스템이 값의 타입이 키와 같음을 보장해주는 설계 방식을 타입 안전 이종 컨테이너 패턴이라고 한다.