2 minute read

item 32 제네릭과 가변인수를 함께 쓸 때는 신중하라.

  • 가변인수: 메서드에 넘기는 인수의 개수를 클라이언트에서 조절이 가능하게 해 준다.
static void dangerous(List<String>... stringLists) {
	List<Integer> intList = List.of(42);
	Object[] objects = stringLists;
	objects[0] = intList;             // (1) 힙 오염 발생
    String s = stringLists[0].get(0); // ClassCastException
}
  • 제네릭 가변인수 배열 매개변수에 값을 저장하게 되면,
    • 매개변수화 타입 변수가 타입이 다른 객체를 참조하게 되어 힙 오염이 발생할 수 있다. (1)
    • 또한, 다른 타입 객체를 참조하는 상황에서 컴파일러의 자동 형변환이 실패할 수 있으므로 타입 안정성이 보장되지 않는다.
    • 따라서 위 두 가지 이유로 인해 이 사용법은 권장되지 않는다.

@SafeVarargs : 경고를 숨기자.

  • 그러나 제네릭이나 파라미터로 가변 인자 매개변수를 받는 메서드가 실무에서는 매우 유용하기 때문에, 오류를 발생시키지는 않고 경고만 발생시킨다.
  • 자바 라이브러리의 예시로는 Arrays.asList(T... a), Collections.addAll(Collection<? super T> c, T... elements), EnumSet.of(E first, E... rest) 등이 있다. 이들은 타입 안정성이 보장되어 있다.

  • 메서드 작성자 측에서 이 메서드가 타입 안정함을 보장할 수 있다면, @SafeVarargs 어노테이션을 달아 클라이언트에서 발생할 경고를 숨길 수 있다.
  • 이 매개변수 배열이 순수하게 인수를 전달하는 일만 한다면, 메서드는 타입 안전하다.

제네릭 매개변수 배열에 다른 메서드가 접근하도록 허용하면 안전하지 않다.

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을 발생시킨다. pickTwo()의 반환 값을 attributes에 저장하기 위해 매개변수화된 가변 인자를 String[]으로 형변환하는 코드를 컴파일러가 자동 생성하기 때문이다.

이처럼 제네릭 매개변수 배열에 다른 메서드가 접근하면 안전하지 않으나, 두 가지 예외가 존재한다.

  1. @SafeVarargs처리한 또 다른 varargs 메서드에 넘기는 것은 안전하다.
  2. 배열 내용을 호출만 하는 (varargs를 받지 않는) 일반 메서드에 넘기는 것도 안전하다.

varargs를 List로 대체하기

@SafeVarargs 애너테이션이 유일한 정답은 아니며, varargs 매개변수를 List 매개변수로 바꿀 수도 있다.(item 28)

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;
}

audience = flatten(List.of(friends, romans, countrymen));
// List.of에도 @SafeVarargs 어노테이션이 달려 있어 이렇게 사용하는 것이 가능하다.

이 방식은 varargs 메서드를 안전하게 작성하는 게 불가능한 상황에서도 사용할 수 있다.

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("좋은", "빠른", "저렴한");
}
// 배열 없이 제네릭만 사용하므로 타입 안전하다.

Leave a comment