이펙티브 자바 - item 32. 제네릭과 가변인수를 함께 쓸 때는 신중하라
on JAVA
이펙티브 자바 - item 32. 제네릭과 가변인수를 함께 쓸 때는 신중하라
가변인수 메서드
가변인수(varargs) 메서드란, 메서드에 넘기는 인수의 개수를 클라이언트가 조절할 수 있게 하는 것이다. 제네릭과 같이 자바 5에서 추가되었다.
void example(String...str) { // ... : 가변인자
for(String a:str)
System.out.println(a);
}
가변인수 메서드를 호출하면, 가변인수를 담기 위한 배열이 자동으로 만들어진다.
따라서 제네릭과 가변인수를 함께 사용하게 되면 제네릭 배열이 생성되고, 아이템 28에서 보았던 힙 오염이 발생하며 아래와 같은 컴파일 경고를 내뱉게 된다.
warning : [unchecked] Possible heap pollution from
parameterized vararg type List<String>
힙 오염 예시
매개변수화 타입의 변수가 타입이 다른 객체를 참조하면, 힙 오염이 발생한다. 아래 예제에서는 get 에서 컴파일러가 보이지 않게 형변환을 하기 때문에, ClassCastException 이 발생하게 된다.
public class Dangerous {
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!"));
}
}
결론적으로 이처럼 타입 안전성이 깨지기 때문에, 제네릭 varargs 배열 매개변수에 값을 저장하는 것은 안전하지 않다.
그럼에도 제네릭 varargs 매개변수를 선언할 수 있게 한 이유
제네릭 배열을 프로그래머가 직접 생성하는 건 허용하지 않으면서 제네릭 varargs 매개변수를 받는 메서드를 선언할 수 있게 한 이유는 실무에서 아주 유용하기 때문이다.
Arrays.asList(T …)
, Collections.addAll(Collection<? super T> c, T… elements)
, EnumSet.of(E first, E… Rest)
가 대표적이다.
위 메서드들은 dangerous 메서드와 달리 타입 안전하다.
@SafeVarargs 애너테이션
@SafeVarargs
는 자바 7 이후 메서드 작성자가 그 메서드가 타입 안전한지 보장할 수 있게 추가된 애너테이션이다. (컴파일러 경고를 없앨 수 있다)
반드시 메서드가 타입 안전한 경우에만 이 annotation을 붙이는 것이 좋다.
메서드가 타입 안전한 경우
가변인수 메서드를 호출 할 때 varargs 매개변수를 담는 제네릭 배열이 만들어지는데, 이때 아래의 두 조건을 만족한다면 타입 안전하다고 말할 수 있다
메서드가 이 배열에 아무것도 저장하지 않는다.
그 배열의 참조가 밖으로 노출되지 않는다.(신뢰할 수 없는 코드가 배열에 접근할 수 없다.
제네릭 varargs 매개변수를 안전하지 않게 사용하는 경우
제네릭 varargs 매개변수 배열에 값을 저장하지 않아도, 다른 메서드가 접근하도록 허용하는것 자체만으로도, 의도치 않은 힙 오염이 발생할 수 있으므로 안전하지 않다.
public class PickTwo {
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("좋은", "빠른", "저렴한");
System.out.println(Arrays.toString(attributes));
}
}
컴파일러는 toArray에 넘길 T 인스턴스 2개를 담을 varargs 매개변수 배열을 만드는 코드를 생성하며, 이 때 배열의 타입은 Object[]이다.
하지만, 컴파일러는 pickTwo의 반환값을 attributes에 저장하기 위해 String[]으로 형변환하는 코드를 자동 생성하게 되고, Object[] 는 String[] 의 하위 타입이 아니므로 이 형변환은 실패하게 되기 때문에, ClassCastException 이 발생한다.
따라서, @SafeVaragrs가 붙어있는 곳이나 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;
}
varargs 배열을 직접 노출 시키지 않고, T타입의 제네릭 타입을 사용하였기 때문에 ClassCastException 또한 발생할 일이 없다. 추가로 제네릭이나 매개변수화 타입의 varargs 매개변수를 받는 모든 안전한 메서드에는 @SafeVarargs
annotation을 달아서 컴파일러 경고를 없애는 것이 좋다.
제네릭 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;
}
item 28의 조언에 따라 varags 매개변수를 List 매개변수로 바꾸는 방법도 존재한다. 이 방식의 장점은 이 메서드의 타입 안전성을 검증할 수 있다는 점이다. 또한 @SafeVarargs
를 달지 않아도 되며 실수로 안전하다고 판단할 걱정도 없다.단점으로는 클라이언트 코드가 살짝 지저분해지고, 속도가 약간 느려질 수 있다.
핵심 정리
- varargs 매개변수는 단순히 파라미터를 받아와 메서드의 생산자(T 타입의 객체를 제공하는 용도)로만 사용하자
- varargs는 read-only라고 생각하고, 아무런 데이터를 저장하지 말자
- varargs 배열을 외부에 리턴하거나 노출하지 말자. 웬만하면 다시 컬렉션(List)에 담아 리턴하는 안전한 방식을 취하자