가변인수(varargs)는 메서드에 넘기는 인수의 개수를 클라이언트가 조절할 수 있도록 해주지만 구현 방식에 허점이 있다.
가변인수 메서드를 호출하게 되면 가변 인수를 담기 위한 배열이 자동으로 하나 만들어진다. 실체화 불가 타입은 런타임 시 타입 정보가 소거되기 때문에 가변인수에 제네릭이나 매개변수화 타입이 포함되면 알기 어려운 컴파일 경고가 발생한다.
매개변수화 타입 (e.g 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
}
varargs 메서드를 호출하면 varargs를 담기 위한 배열이 자동으로 만들어진다고 전술했다. 그 결과로 List<String\>[] stringLists
가 생성되고, 이 배열은 공변이기 때문에 Object[]
로 참조가 가능하다.
-> Object[] objects = stringLists;
제네릭은 런타임 시 타입정보가 소거되기 때문에 타입이 다른List<Integer> intList
의 할당이 가능하다. 결과적으로 서로 다른 타입이 할당되는 힙 오염이 발생했다.
-> Object[0] = intList; //힙 오염 발생
이 후 stringLists
의 0번째 객체를 호출하면 ClassCastException
이 발생하게 된다.
@SafeVarargs는 작성자가 직접 타입 안전함을 보장하는 장치다.
그렇다면 메서드가 타입 안전한지는 어떻게 확인할 수 있을까?
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(b, c);
case 2: return toArray(c, a);
}
throw new AssertiionError();
}
public static void main(String[] args) {
String[] attributes = pickTwo("좋은", "빠른", "저렴한");
}
위 코드는 문제 없이 컴파일 되지만 실행 시 ClassCastException
을 던진다.
String[] attributes = pickTwo("좋은", "빠른", "저렴한");
가 문제가 된다. pickTwo
메서드에서 보이지 않는 형변환 시 발생하는 것이다.
pickTwo
메서드는 varargs 매개변수를 담기 위해 어떤 타입의 객체도 담을 수 있는 가장 구체적인 타입인 Object
배열을 만든다. 즉, pickTwo
메서드는 항상 Object
타입을 반환한다.
Object[]
는 하위 타입이 아니고, String[]
으로 형변환할 수 없기 때문에 ClassCastException
이 발생하는 것이다.
위 예시는 "제네릭 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 어노테이션을 달아서 컴파일러 경고를 없애는 것이 좋다.
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;
}
varargs 매개변수를 List 매개변수로 대체했다.
이 방식은 @SafeVarargs 를 달지 않아도 되기 때문에 실수로 안전한다고 판단할 걱정이 없다. 단점이라면 코드가 조금 지저분하고 속도가 약간 느려질 수 있다는 점이다.
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();
}
.
.
.
List<String> attributes = pickTwo("좋은","빠른","저렴한");
위와 같이 List.of 를 사용할 수도 있다.
위 코드는 배열없이 제네릭만 사용하기 때문에 타입 안전하다.