가변인수
(varargs, variable arguments) 메서드와 제네릭
은 자바 5 때 함께 추가되었으니 서로 잘 어우러지리라 기대하겠지만, 그렇지 않다.
가변인수 메서드를 호출하면 가변인수를 담기 위한 배열이 자동으로 하나 만들어지며, 이 배열은 클라이언트에게 노출된다.
그 결과 varargs 매개변수에 제네릭
이나 매개변수화 타입
이 포함되면 알기 어려운 컴파일 경고가 발생한다.
제네릭과 varargs를 혼용하면 타입 안전성이 깨지게 된다.
따라서 제네릭 varargs 배열 매개변수에 값을 저장하는 것은 안전하지 않다.
그렇다면 제네릭 배열을 프로그래머가 직접 생성하는 건 허용하지 않으면서 제네릭 varargs 매개변수를 받는 메서드를 선언할 수 있게 한 이유는 무엇일까?
이것은 제네릭이나 매개변수화 타입의 varargs 매개변수를 받는 메서드가 실무에서 매우 유용하기 때문이다.
그래서 언어 설계자는 이 모순을 수용하기로 했다.
자바 7에서부터는 @SafeVarargs
애너테이션이 추가되어, 제네릭 가변인수 메서드 작성자가 클라이언트 측에서 발생하는 경고를 숨길 수 있게 되었다.
해당 메서드가 타입 안전하다면 @SafeVarargs
애너테이션을 달아 컴파일러가 경고를 하지 않도록 하자.
단, 안전한게 확실하지 않다면 절대 이 애너테이션을 달아서는 안 된다.
“메서드가 varargs 매개변수를 담은 제네릭 배열에 아무것도 저장하지 않고(그 매개변수들을 덮어쓰지 않고) 그 배열의 참조가 밖으로 노출되지 않는다면(신뢰할 수 없는 코드가 배열에 접근할 수 없다면) 타입 안전하다.”
달리 말하면, varargs 매개변수 배열이 호출자로부터 그 메서드로 “순수하게 인수들을 전달하는 일만 한다면”(varargs의 목적대로만 쓰인다면) 그 메서드는 안전하다.
정리하자면, 다음의 두 가지 조건을 모두 만족하는 제네릭 varargs 메서드는 안전하다.
@SafeVarargs
의 제약사항재정의할 수 없는 메서드
에만 달아야 한다.
→ 재정의한 메서드도 안전할지는 보장할 수 없기 때문이다.
자바 8에서는, 오적 static 메서드
와 final 인스턴스
메서드에만 붙일 수 있다.
자바 9부터는, private 인스턴스 메서드
에도 허용된다.
// 코드 32-2 자신의 제네릭 매개변수 배열의 참조를 노출한다. - 안전하지 않다!
static <T> T[] toArray(T... args) {
return args;
}
toArray 메서드가 반환하는 배열의 타입은 컴파일타임에 결정되는데, 그 시점에는 컴파일러에게 충분한 정보가 주어지지 않아 타입을 잘못 판단할 수 있다.
따라서 자신의 varargs 매개변수 배열을 그대로 반환하면 힙 오염을 이 메서드를 호출한 쪽의 콜스택으로까지 전이하는 결과를 낳을 수 있다.
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("좋은", "빠른", "저렴한"); // ClassCastException 발생
System.out.println(Arrays.toString(attributes));
}
예를 들어, pickTwo 메서드를 다음과 같이 정의한다면, 이 메서드를 본 컴파일러는 toArray에 넘길 T 인스턴스 2개를 담을 varargs 매개변수 배열을 만드는 코드를 생성한다.
이 코드가 만드는 배열의 타입은 Object[]
인데, 이는 pickTwo에 어떤 타입의 객체를 넘기더라도 담을 수 있는 가장 구체적인 타입이기 때문이다.
main 메서드까지 작성한 후 컴파일을 하면 별다른 경고 없이 컴파일이 되지만, 실행하게 되면 ClassCastException
을 던진다.
이는 컴파일러가 pickTwo의 반환값인 Object[]을 attributes에 저장하기 위해 String[]로 형변환하는 코드를 자동으로 생성하였기 때문에 발생하게 된다.
Object[]가 String[]의 하위 타입이 아니기 때문에 형변환이 실패한 것이다.
@SafeVarargs
사용// 코드 32-3 제네릭 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 매개변수를 안전하게 사용하는 전형적인 예시이다.
이 메서드에는 @SafeVarargs
애너테이션이 달려 있으니 선언하는 쪽과 사용하는 쪽 모두에서 경고를 내지 않는다.
제네릭이나 매개변수화 타입의 varargs 매개변수를 받는 모든 메서드에는 @SafeVarargs
를 달자!
단, 안전하지 않은(두 가지 조건을 만족하지 않는) 제네릭 varargs 메서드는 수정하여야 한다.
List
로 대체한 메서드@SafeVarargs
애너테이션이 유일한 답은 아니다.
아이템 28의 조언에 따라 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;
}
매개변수 선언부만 수정이 되었다.
List<? extends T>... lists
→ List<List<? extends T>> lists
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.of
를 활용하면 임의 개수의 인수를 넘길 수 있다.
이렇게 사용할 수 있는 이유는 List.of
에도 @SafeVarargs
애너테이션이 달려 있기 때문이다.
장점
@SafeVarargs
애너테이션을 직접 달지 않아도 되고, 실수로 안전하다고 판단할 걱정이 없다.단점
List.of
사용// 배열 대신 List를 이용해 안전하게 바꿘 PickTwo
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);
}
toArray의 List 버전이 바로 List.of
다.
자바 라이브러리 차원에서 제공하니 직접 작성할 필요도 없다.
배열 없이 제네릭만 사용하므로 타입 안전하다!
📌 핵심 정리
가변인수
와제네릭
은 궁합이 좋지 않다.- 가변인수 기능은 배열을 노출하여 추상화가 완벽하지 못하고, 배열과 제네릭의 타입 규칙이 서로 다르기 때문이다.
- 제네릭 varargs 매개변수는 타입 안전하지는 않지만 허용된다.
- 메서드에 제네릭 혹은 매개변수화된 varargs 매개변수를 사용하고자 한다면, 타입 안전한지 확인한 다음
@SafeVarargs
애너테이션을 달아 사용하는 데 불편함이 없게끔 하자.