아이템 32: 제네릭과 가변인수를 함께 쓸 때는 신중하라
가변인수는 메서드에 넘기는 인수의 개수를 클라이턴트가 조절할 수 있게 해줌
[타입... 변수명] 과 같은 형식으로 사용
가변인수는 파라미터 중 가장 나중에 등장해야함
static String varargsEx (String mail , String ... str ) {
mail += " " ;
for (String s : str ) {
mail += s ;
}
return mail ;
}
가변인수 메서드를 호출하면 가변인수를 담기 위한 배열이 자동으로 하나 만들어진다.
그런데 가변 인수 구현 내부로 감춰야 할 것 같은 이 배열은 클라이언트에 노출된다.
public class Dangerous {
static void dangerous (List <String >... stringLists ) {
Object [] objects = stringLists ; // 클라이언트에게 노출되는 배열!
...
그 결과 varargs 매개변수에 런타임에 실체화 되지 않는 것들이 넘어온다면 위험이 발생할 수 있다.
제네릭 혹은 매개변수화 타입은 실체화되지 않는다
제네릭 혹은 매개변수화 타입은 런타임에 컴파일타임보다 타입 관련 정보를 적게 가지고 있음 (소거)
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 ); // 제네릭으로 인한 형변환 시도 but - ClassCastException
}
public static void main (String [] args ) {
dangerous (List .of ("There be dragons!" ));
}
}
이처럼 가변인수 List<String>... stringLists
처럼 제네릭 배열 매개변수에 값을 저장하는 것은 타입 안전하지 않다.
제네릭 배열을 프로그래머가 직접 생성하는 것은 허용되지 않는다.
//컴파일 되지 않는다.
List <String >[] stringLists = new List <String >[1 ];
그런데 제네릭 varargs 매개변수를 받는 메서드를 선언할 수 있게 한 이유는 무엇일까?
public class Dangerous {
// 제네릭 varargs를 받는 모습 -> 제네릭 배열
static void dangerous (List <String >... stringLists ) {
}
}
제네릭 배열이 정말로 위험하다면 일관성이 떨어지는 부분처럼 보인다.
자바 개발자들은 왜 이런 위험성을 남겨두었을까?
답은 varargs 매개변수를 받는 메서드가 유용한 부분이 있기 때문
그래서 언어 설계자들은 이러한 모순을 수용하기로 했다.
실제로 이러한 모순적인 메서드는 여럿 제공되고 있음
Arrays .asList (T ... a )
Collections .addAll (Collection <? super T > c , T ... elements )
EnumSet .of (E first , E ... rest )
자바 7 전에는 제네릭 가변인수 메서드의 작성자가 호출자 쪽에서 발생하는 경고에 대해서 해줄 수 있는 일이 없었다.
따라서 이런 메서드는 사용하기에 꺼림칙했고 사용자들은 이 경고들을 그냥 두거나@SuppressWarnings("unchecked")
애너테이션을 달아 경고를 숨겼다.
@SafeVarargs
애너테이션은 메서드 작성자가 그 메서드가 타입 안전함을 보장하는 장치다.
컴파일러는 이 약속을 믿고 그 메서드가 안전하지 않을 수 있다는 경고를 더 이상 하지 않는다.
따라서 메서드가 안전한 게 확실하지 않다면 절대 @SafeVarargs
애너테이션을 달아서는 안 된다.
varargs 매개변수 배열을 안전하게 사용하는 방법
varargs 매개변수 배열이 호출자로부터 그 메서드로 순수하게 인수들을 전달하는 일만 한다면 그 메서드는 안전하다.
varargs의 목적대로만 쓰인다면 안전하다는 것이다.
varargs매개변수 배열이 안전하지 않은 경우
varargs 매개변수 배열에 아무것도 저장하지 않고도 타입 안전성을 깰 수도 있으니 주의해야 한다.
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 (); // 도달할 수 없다.
}
이 메서드를 본 컴파일러는 toArray에 넘길 T 인스턴스 2개를 담을 varargs 매개변수 배열을 만드는 코드를 생성한다.
pickTwo메서드가 만드는 배열의 타입은 항상 Object[] 타입 배열이다.
pickTwo에 어던 타입의 객체를 넘기더라도 담을 수 있는 가장 구체적인 타입이 Object이기 때문
public static void main (String [] args ) {
String [] attributes = pickTwo ("좋은" , "빠른" , "저렴한" );
}
pickTwo를 사용하는 클라이언트 코드를 보자
별다른 경고 없이 컴파일 되지만 실행하면 ClassCastException
을 던진다.
형변환하는 코드를 컴파일러가 자동 생성할 때 Object[]는 String[]의 하위타입이 아니기 때문에 형변환에 실패하는 것이다.
이 실패가 다소 황당하게 느껴질 수 있을 것이다.
힙 오염을 발생시킨 진짜 원인인 toArray로부터 두 단계나 떨어져 있고, varargs 매개변수 배열은 실제 매개변수가 저장된 후로 변경된 적이 없으니 말이다.
이 예는 제네릭 varargs 매개변수가 배열에 다른 메서드가 접근하도록 허용하면 안전하지 않다라는 점을 시사한다.
단 예외가 두가지 있다.
@SafeVarargs
로 제대로 애노테이트 된 또 다른 메서드에 넘기는 경우 안전
배열 내용의 일부 함수를 호출만 하는 (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 );
}
}
위의 코드는 모든 원소를 하나의 리스트로 옮겨 담아 반환
@SafeVarargs 애너테이션이 달려 있으니 선언하는 쪽과 사용하는 쪽 모두에서 경고를 내지 않음
제네릭이나 매개변수화 타입의 varargs 매개변수를 받는 모든 메서드에 @SafeVarargs를 달라.
그래야 사용자를 헷갈리게 하는 컴파일러 경고를 없앨 수 잇다.
이 말은 안전하지 않은 varargs 메서드는 절대 작성해서는 안된다는 뜻
힙 오염 경고가 뜨는 메서드가 있다면, 그 메서드가 진짜 안전한지 점검하라
flatten (List .of (friends , romans , coutrymen ), List .of (wo , wa , course ));
안전하다.
안전한 이유는 List.of에 @SafeVarargs
가 달려있기 때문
미리 구현된 @SafeVarargs 애너테이션을 우리가 직접 달지 않아도 되며, 실수로 안전하다고 판단할 걱정도 없다.
이 방식을 안전하지 않았던 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 );
}
}