티스토리 뷰
32. 제네릭과 가변인수를 함께 쓸 때는 신중하라
가변인수(varags)가 무엇인가요?
private static String[] check(String... arr){
return arr;
}
이 친구이다. 메소드의 파라미터로 여러개의 변수를 받을 수 있게 만들어 주는 기능이다. 그렇다면 왜 제네릭과 쓸 때 주의해야 하느냐? 바로 가변인수로 받은 파라미터들을 배열을 생성해 담기 때문이다. 아아.. 배열.. 이미 지난 아이템에서 부터 제네릭과 배열의 안 좋은 궁합에 대해 입이 닳도록 이야기해 왔다.
// 제네릭 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;
objects[0] = intList; // 힙 오염 발생
String s = stringLists[0].get(0); // ClassCastException
}
public static void main(String[] args) {
dangerous(List.of("There be dragons!"));
}
}
우리가 제네릭 배열을 직접 생성하는 것은 막아놨지만 가변인수의 경우는 풀어놨다. 따라서 위의 소스코드를 시행하면 런타임에 에러가 발생한다. 가변인수는 이를 왜 허용해 줬을까? 실무에서 매우 유용해 이런 모순을 허락하기로 했다고 한다. Arrays.asList(T...a)와 같이 제공되는 api로 제공되는 코드가 예시이다. 이렇게 제공되는 api는 타입이 안전하다.
비검사 형변환이 발생하면 unchecked warning이 발생하는데 코드 작성에서 실수하지 않았으면 이를 @SuppressWarnings("unchecked")로 경고를 제거하는것이 좋다. 이는 매우 지루한 일이었는데 @SafeVarargs가 등장하면서 수고가 훨씬 줄었다.
타입이 안전한지는 어떻게 알 수 있을까? 가변인수로 생성된 배열을 다른 값으로 덮어쓰지 않고, 생성된 배열의 레퍼런스를 외부에 노출하지 않는다면 타입 안정성이 지켜진다.
// 미묘한 힙 오염 발생 (193-194쪽)
public class PickTwo {
// 코드 32-2 자신의 제네릭 매개변수 배열의 참조를 노출한다. - 안전하지 않다! (193쪽)
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) { // (194쪽)
String[] attributes = pickTwo("좋은", "빠른", "저렴한");
System.out.println(Arrays.toString(attributes));
}
}
위의 코드에서 pickTwo는 제네릭 파라미터 세개를 받아 이를 가변인수 메소드인 toArray에게 넘겨 해당 배열을 반환한다. 하지만 제네릭은 컴파일 타임에서만 정보가 존재하고 런타임에 들어서면 타입 정보가 사라진다. pickTwo로 들어온 T는 타입 정보가 사라져 모든 값을 담을 수 있는 Object로 wrapping된다. 따라서 toArray는 Object[]를 반환하게 되고 이는 main문에서 ClassCastException이 발생한다. Ojbect[]를 String[]으로 형변환 할 수 없기 때문이다. 이는 가변인수 배열의 참조를 외부에 노출하면 타입 안정성이 보장되지 않음을 보인다.
// 코드 32-3 제네릭 varargs 매개변수를 안전하게 사용하는 메서드 (195쪽)
public class FlattenWithVarargs {
@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;
}
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);
}
}
위의 가변인수 코드는 타입 안정하다. lists의 참조를 외부에 공개하지 않았고, 배열의 내부 값을 수정하지 않았다. 우리는 타입 안정한지 알지만 컴파일러는 모르기에 @SafeVarags를 통해 경고를 제거하도록하자.
// 코드 32-4 제네릭 varargs 매개변수를 List로 대체한 예 - 타입 안전하다. (195-196쪽)
public class FlattenWithList {
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;
}
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);
}
}
매개 변수를 List로 변경한 코드이다. 타입 안정적이며 제네릭과 궁합이 잘 맞는다.
'Book > Effective Java' 카테고리의 다른 글
Effective Java - Item 35. ordinal 메서드 대신 인스턴스 필드를 사용하라 (0) | 2023.05.28 |
---|---|
Effective Java - Item 34. int 상수 대신 열거 타입을 사용하라 (0) | 2023.05.24 |
Effective Java - Item 28. 배열보다는 리스트를 사용하라. (1) | 2023.05.22 |
Effective Java - Item 26. 로 타입은 사용하지 말라 (0) | 2023.04.21 |
Effective Java - Item 24. 멤버 클래스는 되도록 static으로 만들어라 (0) | 2023.03.22 |