제네릭과 가변인수를 함께 쓸 때는 신중하라.
제네릭과 가변인수는 생각보다 잘 어우러지지 않는다. 아래코드를 매개변수화 타입을 적용한다면 어떻게 될까? 제네릭은 실체화 불가 타입이기 때문에 타입 관련 정보를 적게 담고 있어 컴파일 타임에 에러를 못 낼 수 있다. 이처럼 타입 안전성이 깨지니 제네릭 varargs배열 매개변수에 저장하는 것은 안전하지 않다.
static void dangerous(List<String>... stringLists) {
List<Integer> intList = List.of(42);
Object[] objects = stringLists;
objects[0] = intList; // 힘오염 발생
String s = stringLists[0].get(0); // ClassCastException
}
제네릭 가변인수 만들어보기
아래 코드는 메서드에 인수를 넘기는 컴파일 타입에 결정되는데 그 시점에는 컴파일러에게 충분한 정보가 주어지지 않아 타입을 잘못 판단할 수 있다.
static <T> T[] toArray(T... args) {
return args;
}
public static void main(String[] args) {
String[] attributes = pickTwo("좋은", "빠른", "저렴한");
}
위 코드는 에러가 난다. 반환 타입은 Object[]이지만, T...는 컴파일러에서 자동으로 String[]으로 형변환하기 때문이다. Object[]는 String[]의 하위타입이 아니기 때문에 에러가 난다.
안정적으로 제네릭 가변인수 사용하는 방법
@SageVarargs로 된 varargs 메서드에 쓰면 컴파일러 경고를 없앨 수 있다. 하지만 기본적으로 안전하지 않기 때문에 쓰는 것이므로 varargs를 안 쓰는 것이 좋다.
@SafeVarargs
static <T> List<T> flatten(List<? extends T>... lists) {
List<T> result = new ArrayList<>();
for(List<? extends T> list : lists)
result.addAll(list);
return result;
}
점검 사항
- vargargs 매개변수 배열에 아무것도 저장하지 않는다.
- 그 배열(혹은 복제본)을 신뢰할 수 없는 코드에 노출하지 않는다.
이 배열 내용의 일부 함수를 호출만 하는 (vargargs가 아닌) 일반 메서드에 넘기는 것이 안전하다.
static <T> List<T> flatten(List<List<? extends T>> lists) {
List<T> result = new ArrayList<>();
for (List<? extends T> list : lists)
result.addAll(list);
return result;
}
타입 안전 이종 컨테이너를 고려하라
Map<K,V>는 키와 값의 타입을 뜻하는 2개만 필요한 식이다. 데이터 베이스의 경우에는 행과 임의의 개수의 열을 가질 수 있는데 모두 열을 타입 안전하게 이용 할 수 있는 방법이 필요하다. 방법은 컨테이너 대신 키를 매개변수화한 다음, 컨테이너에 값을 넣거나 뺄 때 시스템이 값의 타입이 키와 같음을 보장해줄 것이다. 이것은 타입 안전 이종 컨네이터 패턴이라 한다.
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));
}
}
Map<Class<?>, Object>
Class<?>는 비한정적 와일드 카드라서 아무것도 넣을 수 없는 것처럼 보이지만 실제로는 키가 비한정적 와일드 카드이다. 따라서 중첩된 와일드카드 타입이므로 모든 키가 서로 다른 매개변수화 타입 일 수 있다는 뜻이다.
getFavorite 메서드
getFavorite은 값이 Object이므로 타입 캐스팅을 해주어야 한다. cast는 함수는 동적으로 형변환 해준다.
Favorties 클래스의 제약
첫 번째, putFavorite함수를 좀 더 안정적으로 바꿔야 한다. instance를 Class객체가 아닌 로타입으로 넘기면 안정성이 깨진다. put할때도 형변환을 시켜주자.
public <T> void putFavorite(Class<T> type, T instance) {
favorites.put(Objects.requireNonNull(type), type.cast(instance));
}
java.util.Collections에는 checkedSet, checkedList, checkedMap같은 메서드가 있는데, 바로 이 방식을 적용한 컬렉션 래퍼들이다. 비슷한 방식으로 type에 대한 체크를 하고 있다.
E typeCheck(Object o) {
if (o != null && !type.isInstance(o))
throw new ClassCastException(badElementMsg(o));
return (E) o;
}
두 번째, 제약은 실체화 불가 타입에는 사용 할 수 없다. List<String>용 Class 객체를 얻을 수 없어 허용되지 않는다.
한정하는 방법
static Annotation getAnnotation(AnnotationElement element,
String annotationTypeName) {
Class<?> annotationType = null;
try {
annotationType = Class.forName(annotationTypeName);
} catch (Exception ex) {
throw new IllegalArgumentException(ex);
}
return element.getAnnotation(
annotationType.asSubclass(Annotation.class)
);
}
단순히 <? extends T>로 형변환을 할 수도 있겠지만 비검사이므로 컴파일하는 도중 경고가 나올것이다. 하지만 asSubClass를 사용한다면 오류나 경고 없이 컴파일이 된다.
@SuppressWarnings("unchecked")
public <U> Class<? extends U> asSubclass(Class<U> clazz) {
if (clazz.isAssignableFrom(this))
return (Class<? extends U>) this;
else
throw new ClassCastException(this.toString());
}
'Language > Effective java' 카테고리의 다른 글
열거 타입과 애너테이션 (0) | 2022.07.18 |
---|---|
열거 타입과 애너테이션 (0) | 2022.07.13 |
제네릭 (0) | 2022.06.19 |
제네릭 (0) | 2022.06.18 |
클래스와 인터페이스 (0) | 2022.06.17 |
댓글