이펙티브 자바 아이템 32 - 제네릭과 가변인수를 함께 쓸 때는 신중하라 - 핵심 정리
아이템 32 - 제네릭과 가변인수를 함께 쓸 때는 신중하라 - 핵심 정리
이 글은 백기선 님의 이펙티브 자바 강의와 이펙티브 자바 3 / E 편을 참고하여 작성하였습니다.
가변인수 ...
는 매서드를 사용하는 클라이언트에서 파라미터를 몇개 보낼지 선택하는 것이다.
// 제네릭 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!"));
}
}
클라이언트가 넘기고 싶은 인수를 마음껏 넘길 수 있다.
이 가변인수를 제네릭 타입과 같이 사용하면 문제가 발생한다.
제네릭은 성격상 배열과 맞지 않는다.
때문에 제네릭 타입의 배열을 정의하는 걸 컴파일러가 막아준다.
그러나 가변인자에서는 내부적으로는 제네릭의 배열이 만들어지는 경우 가 있다.
가변인자와 제네릭을 같이 사용하면 제네릭 타입의 배열 이 내부적으로 만들어진다.
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
}
위와같이 (List<String>... stringLists)
로 선언을 하면 컴파일 에러는 아니지만 경고가 발생한다.
제네릭을 사용한 가변인자 떄문에 힙 메모리 가 오염될 수 있다는 메세지이다.
List<Integer> intList = List.of(42);
Object[] objects = stringLists;
objects[0] = intList; // 힙 오염 발생
String s = stringLists[0].get(0); // ClassCastException
실제로 위의 코드는 컴파일 에러는 발생하지 않지만 굉장히 위험한 코드이다.
List
의 배열을 Object
에 할당한다.
배열은 공변이기 때문에 List
의 배열을 Object
배열에 할당할 수 있다.
objects[0] = intList
처럼 Object
배열이기 때문에 List<Integer>
를 할당할 수 있다.
stringLists[0].get(0)
코드를 실행할 때 에러가 발생한다.
stringLists[0].get(0)
코드 실행시 컴파일 시 String
으로 캐스팅하는 코드를 넣어준다.
stringLists
는 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
}
위의 코드는 의도적으로 List<Integer>
를 넣음으로써 힙 오염을 발생시켰다.
이런 일을 하지 않고 정상적으로 문자열을 넣으면 안전하게 사용할 수 있다.
// 코드 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);
}
}
위의 코드처럼 가변인자를 안전하게 사용하는 경우에는 자바 7버전 부터 추가된 @SafeVarargs
를 사용하면
가변인자는 안전하게 사용되고 있다. 라는 뜻이된다.
그 이외의 경고 메세지가 발생해도 컴파일 타임에 확인할 수 있다.
@SuppressWarnings("unchecked")
를 사용하면 다른 unchecked
경고 역시 무시되기 떄문에
제네릭 가변인자 와 관련된 경고 메세지는 @SafeVarargs
를 사용하여 무시하는 걸 권장한다.
제네릭 가변인자 를 안전하게 사용하는 경우는 다음 두가지가 있다.
- 제네릭 가변인자 에 아무것도 넣지 않는다.
@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;
}
위의 코드처럼 가변인자로 받은 파라미터에 아무것도 저장하지 않고,
전달받은 그대로 메서드를 호출하거나 다른 리스트에 넣는다면 안전하게 사용하고 있는 것이다.
- 제네릭 가변인자 를 컴파일러가 내부적으로 만든 제네릭 배열을 밖에 노출하면 안된다.
제네릭 가변인자 를 절대로 밖으로 노출하면 안된다.
// 미묘한 힙 오염 발생 (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));
}
}
위의 코드는 가변인자로 전달받은 제네릭 배열 을 그대로 리턴한다.
static <T> T[] toArray(T... args) {
return args;
}
컴파일러는 toArray
의 리턴타입을 Object
로 판단한다.
public static void main(String[] args) { // (194쪽)
String[] attributes = pickTwo("좋은", "빠른", "저렴한");
System.out.println(Arrays.toString(attributes));
}
결과적으로 리턴타입은 Object
가 되지만 값은 String
으로 받고있기 때문에 에러가 발생한다.
이 문제에 대한 근본적인 원인은 toArray
에서 제네릭 배열 을 리턴했기 떄문이다.
위와같은 상황은 안전학지 않은 상황이기 떄문에 @SafeVarargs
를 사용하면 안된다.
// 배열 대신 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);
}
}
안전하게 사용하기 위해 가변인자 대신 List.of
를 사용했다.
static <E> List<E> of(E e1, E e2) {
return new ImmutableCollections.List12<>(e1, e2);
}
List.of
내부에서는 매개변수를 받아 새로운 List
를 만들어 리턴하도록 되어있다.
// 코드 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);
}
}
위의 코드도 안전하게 사용하기 위해 아래와 같이 수정한다.
// 코드 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<List<? extends T>>
를 사용하도록 변경한다.
flatten
는 값을 받아 어딘가에 넣어주는 프로듀서 이기 떄문에 <? extends T>
를 사용한다.
이렇게 가변 인자 대신 List
를 사용하면 @SafeVarargs
를 사용할 일이 없어진다.
또한 실수로 안전하지 않는 코드에 @SafeVarargs
를 붙이는 일도 없어지므로 코드가 상당히 안전 해진다.
그리고 경고 메세지가 발생함으로 나오는 고민이 없어진다.
단점으로는 List<List<? extends T>>
와 같이 코드가 조금은 복잡해보이는 점 하나가 있다.
가변 인자 대신 List
를 사용하는 것이 단점보다는 장점이 많으므로 가변 인자 보다는 List
를 더 잘 활용하도록 하자.
제네릭 타입 의 가변인자를 리턴하는 것이 항상 문제가 되지는 않는다.
이러한 상황으로는 @SafeVarargs
가 붙어있는 가변 인자 를 안전하게 사용하는 곳이거나
가변 인자 를 매개변수로 받지 않는 일반적인 메서드에 전달한다면 안전한 상황이지만
힙 오염 이 발생할 여지는 있기 때문에 제네릭 타입 의 가변인자를 전달하지 않는 것이 좋다.
더 나아가 가변 인자 대신 List
를 사용하는 것이 가장 안전하고 List<List<? extends T>>
를 사용하는게
타입 안정성을 확보하는 좋은 방법이다.