가변인수와 제네릭은 궁합이 좋지 않다. 가변인수 기능은 배열을 노출하여 추상화가 완벽하지 못하고, 배열과 제네릭의 타입 규칙이 서로 다르기 때문이다. 제네릭 varargs 매개변수는 타입 안전하지는 않지만, 허용된다. 메서드에 generic (혹은 parameterized type) varargs 매개변수를 사용하고자 한다면, 먼저 그 메서드가 타입 안전한지 확인한 다음 @SafeVarargs 애너테이션을 달아 사용하는데 불편함이 없게끔 하자
확인 규칙 :
- 메서드가 이 배열에 아무것도 저장하지 않고(그 매개변수들을 덮어쓰지 않고)
- 그 배열의 참조가 밖으로 노출되지 않는다면(신뢰할 수 없는 코드가 배열에 접근할 수 없다면)
방법 1: @SafeVarargs사용 규칙안전하지 않은 예시 (varargs 배열에 다른 메서드가 접근하도록 허용하면 안전x)안전한 예시방법 2: varargs 매개변수를 List 매개변수로 바꾸기
- 메서드를 선언할 때 실체화 불가 타입(List<E>[], new List<String>[], new E[])으로 varargs 매개변수를 선언하면 컴파일러가 경고를 보냄
warning: [unchecked] Possible heap pollution from parameterized vararg type List<String>
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 } public static void main(String[] args) { dangerous(List.of("There be dragons!")); }
방법 1: @SafeVarargs
- @SafeVarargs 애너테이션은 메서드 작성자가 그 메서드가 타입 안전함을 보장하는 장치임
- 해당 애너테이션이 있으면 컴파일러가 그 메서드가 안전하지 않을 수 있다는 경고를 더이상 하지 않음(클라이언트 측에서)
사용 규칙
- 제네릭이나 parameterized type(eg. List<String>) 의 varargs 매개변수를 받는 모든 메서드에 @SafeVarargs 를 달라
- 메서드가 안전한지 어떻게 확신??
- 가변인수 메서드를 호출할 때 varargs 매개변수를 담는 제네릭 배열이 만들어지는데
- 메서드가 이 배열에 아무것도 저장하지 않고(그 매개변수들을 덮어쓰지 않고)
- 그 배열의 참조가 밖으로 노출되지 않는다면(신뢰할 수 없는 코드가 배열에 접근할 수 없다면)
- ⇒ 타입 안전 . @SafeVarargs 붙이기
- 즉, varargs 매개변수 배열이 호출자로부터 그 메서드로 순수하게 인수들을 전달하는 일만 한다면(varargs의 목적대로만 쓰인다면) 그 메서드는 안전함
@SafeVarargs 애너테이션은 재정의할 수 없는 메서드에만 달아야 한다. 재정의한 메서드도 안전할 지는 보장할 수 없기 때문임.
안전하지 않은 예시 (varargs 배열에 다른 메서드가 접근하도록 허용하면 안전x)
static <T> T[] toArray(T... args) { return args; } static <T> T[] pickTwo(T a, T b, T c) { switch(ThreadLocalRandom.current().nextInt(3)) { case 0: return toArray(a, b); case 1: return toArray(a, c); case 2: return toArray(b, c); } throw new AssertionError(); // 도달할 수 없다. } public static void main(String[] args) { String[] attributes = pickTwo("좋은", "빠른", "저렴한"); // pickTwo가 Object[]를 반환 }
- toArray 메서드를 본 컴파일러는 T 인스턴스 2개를 담을 varargs 매개변수 배열을 만드는 코드를 생성함. 이 코드가 만드는 배열의 타입은 Object[]인데, pickTwo에 어떤 타입의 객체를 넘기더라도 담을 수 있는 가장 구체적인 타입이기 때문임
- 아무런 문제가 없어보이는 메서드라 별다른 경고 없이 컴파일이 됨. 하지만 실행하려 들면 ClassCastException을 던짐
- pickTwo의 반환값을 attributes에 저장하기 위해 String[]로 형변환하는 코드를 컴파일러가 자동 생성한다는 점을 놓쳐서 발생하게 됨
- Object[]는 String[]의 하위 타입이 아니므로 형 변환은 실패함
- 예외
- @SafeVarargs로 제대로 애노테이트 된 또다른 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; }
방법 2: varargs 매개변수를 List 매개변수로 바꾸기
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; } audience = flatten(List.of(friends, romans, countrymen));