아이템 32. 제네릭과 가변인수를 함께 쓸 때는 신중하라
제네릭 varargs 배열 매개변수에 값을 저장하는 것은 안전하지 않다.
아이템 28의 @SafeVarags 내용과 연결
거의 모든 제네릭과 매개변수화 타입은 실체화되지 않는다. 제네릭은 배열로 사용할 수 없다.
하지만 가변인수 메서드를 호출하면 가변인수를 담기 위한 배열이 자동으로 하나 만들어진다. 우리가 직접 만들 수는 없지만 자바 내부적으로는 제네릭의 배열이 만들어지는 것이다.
그래서 메서드를 선언할 때 실체화 불가 타입으로 varargs 매개변수를 선언하면 컴파일러가 경고를 보낸다.
warning : Possible heap pollution from parameterized vararg type
매개변수화 타입의 변수가 타입이 다른 객체를 참조하면 힙 오염이 발생하기 때문이다.
✔️ Heap pollution
occurs when a variable of a parameterized type refers to an object that is not of that parameterized type.
(ref : https://docs.oracle.com/javase/tutorial/java/generics/nonReifiableVarargsType.html#heap_pollution)
다른 타입 객체를 참조하는 상황에서는 컴파일러가 자동 생성한 형변환이 실패할 수 있으니, 제네릭 타입 시스템이 약속한 타입 안전성의 근간이 흔들려버린다.
// 제네릭 varargs 배열 매개변수에 값을 저장하는 것은 안전하지 않다. (191-192쪽)
public class Dangerous {
// 코드 32-1 제네릭과 varargs를 혼용하면 타입 안전성이 깨진다! (191-192쪽)
static void dangerous(List<String>... stringLists) {
List<Integer> intList = List.of(42);
Object[] objects = stringLists; // 배열은 공변이니까 Object[]에 할당 가능하다.
objects[0] = intList; // 힙 오염 발생
String s = stringLists[0].get(0); // ClassCastException 발생. 보이지 않는 형변환이 숨어있다.
}
public static void main(String[] args) {
dangerous(List.of("There be dragons!"));
}
}
그럼 왜 제네릭 배열을 프로그래머가 직접 생성하는 건 허용하지 않으면서 제네릭 varargs 매개변수를 받는 메서드 선언은 허용할까?
제네릭이나 매개변수화 타입의 arargs 매개변수를 받는 메서드가 실무에서 매우 유용하기 때문이다.
// 코드 32-3 제네릭 varargs 매개변수를 안전하게 사용하는 메서드 (195쪽)
public class FlattenWithVarargs {
@SafeVarargs // 가변인자는 안전하게 사용되고 있다. 라는 annotation. 클라이언트 측에서 발생하는 경고를 숨길 수 있게 됨. SuppressWarnings 보다 구체적임.
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;
}
public static void main(String[] args) {
List<Integer> flatList = flatten(
List.of(1, 2), List.of(3, 4, 5), List.of(6,7));
System.out.println(flatList);
}
}
그렇다면 메서드가 안전한지는 어떻게 확신할 수 있을까?
- 메서드가 이 배열에 아무것도 저장하지 않고 (그 매개변수들을 덮어쓰지 않고)
- 그 배열의 참조가 밖으로 노출되지 않는다면 (신뢰할 수 없는 코드가 배열에 접근할 수 없다면)
조건을 만족한다면 타입 안전하다. (만약 위 flatten 메서드에서 lists를 리턴하게 된다면 참조가 노출된다.)
즉, varargs 매개변수 배열이 호출자로부터 그 메서드로 순수하게 인수들을 전달하는 일만 한다면 (varargs의 목적대로만 쓰인다면) 그 메서드는 안전하다.
▼ 제네릭 매개변수 배열의 참조를 노출하는 예제코드
// 미묘한 힙 오염 발생 (193-194쪽)
public class PickTwo {
// 코드 32-2 자신의 제네릭 매개변수 배열의 참조를 노출한다. - 안전하지 않다! (193쪽)
static <T> T[] toArray(T... args) {
return args;
}
// T타입 인수 3개를 받아 그 중 2개를 무작위로 골라 담은 배열을 반환하는 메서드
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); // 컴파일러는 toArray의 리턴타입을 Object[]로 판단한다.
case 2: return toArray(b, c); // 즉 pickTwo는 항상 Object[] 타입 배열을 반환한다ㅣ.
}
throw new AssertionError(); // 도달할 수 없다.
}
public static void main(String[] args) { // (194쪽)
String[] attributes = pickTwo("좋은", "빠른", "저렴한"); // ClassCastException 발생!
System.out.println(Arrays.toString(attributes));
}
}
pickTwo 메서드는 항상 Object[] 타입의 배열을 반환하는데, 이를 String[]으로 변환하는 코드를 컴파일러가 자동 생성하므로 ClassCastException이 발생한다.
▼ 제네릭 varargs 매개변수를 안전하게 사용하는 메서드 예제
// 배열 대신 List를 이용해 안전하게 바꿘 PickTwo (196쪽)
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("좋은", "빠른", "저렴한");
System.out.println(attributes);
}
}
안전하지 않은 varargs메서드는 절대 작성해서는 안 되며, 작성한 제네릭이나 매개변수화 타입의 varargs 매개변수를 받는 모든 (안전한) 메서드에 @SafeVarags를 달라.
✔️ varargs 매개변수 배열에 아무것도 저장하지 않는다.
✔️ 그 배열(혹은 복제본)을 신뢰할 수 없는 코드를 노출하지 않는다.
@SafeVarargs 애너테이션은 재정의할 수 없는 메서드에만 달아야 한다. 재정의한 메서드도 안전할지는 보장할 수 없기 때문이다.
SafeVarargs 애너테이션이 유일한 정답은 아니며, varargs 매개변수를 List 매개변수로 바꿀 수도 있다.
public class FlattenWithList {
static <T> List<T> flatten(List<List<? extends T>> lists) { // 프로듀서이므로 extends
List<T> result = new ArrayList<>();
for (List<? extends T> list : lists)
result.addAll(list);
return result;
}
public static void main(String[] args) {
List<Integer> flatList = flatten(List.of(
List.of(1, 2), List.of(3, 4, 5), List.of(6,7)));
System.out.println(flatList);
}
}