Item32. 제네릭과 가변인수를 함께 쓸 때는 신중하라!
- 가변인수 메서드와 제네릭은 잘 어우러지지 않는다.
가변 인수의 문제점
- 가변인수는 메서드에 넘기는 인수의 개수를 클라이언트가 조절할 수 있다. 구현 방식에 허점이 있다.
- 메서드 호출 시 가변인수를 담기 위한 배열 생성
- 이 배열을 클라이언트에 노출시키는 문제가 생김.
- 메서드 호출 시 가변인수를 담기 위한 배열 생성
- 위와 같은 문제로 인해 가변인수(varargs) 매개변수에 제네릭이나 매개변수화 타입이 포함되면 알기 어려운 컴파일 경고가 발생한다.
실체화 불가 타입
- 런타임에서 컴파일타임보다 타입 관련 정보를 적게 담고 있는다.
- 거의 모든 제네릭과 매개변수화 타입은 실체화되지 않는다.
- 메서드 선언 시 실체화 불가 타입으로 가변인수 매개변수를 선언하면 컴파일러가 경고를 보낸다.
- 가변인수 메서드도 마찬가지로 varargs 매개변수가 실체화 불가 타입이라 추론될 경우, 경고를 준다.
- 그 경고는 힙 오염 발생이다.
- 다른 타입 객체 참조 시, 컴파일러가 자동 생성한 형변환의 실패 가능성이 있어 제네릭이 약속한 타입 안전성의 근간이 흔들린다.
- 그 경고는 힙 오염 발생이다.
- 가변인수 메서드도 마찬가지로 varargs 매개변수가 실체화 불가 타입이라 추론될 경우, 경고를 준다.
제네릭과 가변인수 혼용
1
2
3
4
5
6
static void dangerous(List<String>... stringLists) {
List<Integer> intList = List.of(42);
Object[] objs = stringLists;
objs[0] = intList; // 힙 오염 발생
String s = stringLists[0].get(0); // ClassCastException
}
- 마지막 줄에 컴파일러가 생성한 (보이지 않는) 형변환이 숨어있다.
- 이 경우 형변환에 실패해 에러를 던진다.
- 타입 안전성을 위해 제네릭 varargs 배열 매개변수에 값을 저장하는 것은 안전하지 않다.
제네릭 배열 생성을 허용하지 않는데, 제네릭 varargs 매개변수 받는 메서드 선언을 허용하는 이유는?
- 제네릭이나 매개변수화 타입의 varargs 매개변수를 받는 메서드가 실무에서 매우 유용히 사용된다.
- 그래서 언어 설계자는 이 모순을 수용했다.
- Arrays.asList(T… a), Collections.addAll(Collection<? super T> c, T…elements), EnumSet.of(E first, E… rest)와 같은 대표적인 예가 존재한다.
- 위에 기입한 예는 위험한 메서드들과는 달리 타입 안전하다.
@SafeVarargs 애너테이션
- 메서드 작성자가 그 메서드가 타입 안전함을 보장하는 장치다.
- 제네릭 가변인수 메서드 작성자가 클라이언트 측에서 발생하는 경고를 숨길 수 있다.
- 메서드 안전이 보장되지 않으면 절대 @SafeVarargs 애너테이션을 달아서는 안 된다.
메서드 안전을 확신할 수 있는 경우
varargs 매개변수 배열에 아무것도 저장하지 않는 경우
배열의 참조가 밖으로 노출되지 않는 경우
- 정리해보면, varargs 매개변수 배열이 (just) 인수전달 목적으로만 사용되면 메서드는 안전하다.
안전하게 제네릭 varargs 매개변수 사용하기
- @SafeVarargs로 제대로 애노테이트된 또 다른 varargs 메서드에 넘기는 것
- 그저 배열 내용의 일부 함수를 호출만 하는 일반 메서드에 넘기는 것
안전한 사용의 예
1 2 3 4 5 6 7
@SafeVarargs static <T> List<T> saveCapsule(List<? extends T>... lists) { List<T> result = new ArrayList<>(); for (List<? extends T> list : lists) result.addAll(list); return result; }
- 임의 개수의 리스트를 인수로 받아, 받은 순서대로 그 안의 모든 원소를 옮겨 담아 반환한다.
@SafeVarargs 애너테이션 사용 규칙
- 제네릭이나 매개변수화 타입의 varargs 매개변수를 받는 모든 메서드에 @SafeVarargs 애너테이션을 달자.
- 이 말은 동시에 안전하지 않은 varargs 메서드는 절대 작성해서는 안된다는 뜻.
List.of를 이용하는 방법
- varargs 매개변수를 List 매개변수로 바꾼다.
- 정적 팩터리 메서드인 List.of를 활용해 임의 개수의 인수를 넘길 수 있다.
- List.of 역시 @SafeVarargs 애너테이션이 달려있기 때문에 활용 가능하다.
List.of를 이용한 예제
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class SafePickTwo {
static <T> List<T> pickTwo(T a, T b, T c) {
switch(ThreadLocalRandom.current().nextInt(3)) {
case 0: return List.of(a, b);
case 1: return List.of(a, c);
case 2: return List.of(b, c);
}
throw new AssertionError();
}
public static void main(String[] args) {
List<String> attributes = pickTwo("좋은", "빠른", "저렴");
}
}
- 이 방식의 장점은 컴파일러가 메서드의 타입 안전성을 검증할 수 있다는 데에 있다.
- 위 예제의 결과 코드는 배열 없이 제네릭만 사용하므로 타입 안전하다.
핵심 정리
- 가변인수와 제네릭은 궁합이 좋지 않다.
- 가변인수 기능은 배열을 노출한다. 고로, 추상화가 완벽하지 못하다.
- 배열과 제네릭의 타입 규칙이 서로 다르다.
- 제네릭 varargs 매개변수는 타입 안전하지는 않다. 그러나 허용된다.
- 메서드에서 제네릭(또는 매개변수화된) varargs 매개변수 사용을 하려는 경우
- 그 메서드가 타입 안전한지 확인한다.
- @SafeVarargs 애너테이션을 달아 사용의 불편함을 제거한다.
Comments powered by Disqus.