이펙티브 자바 - item 32. 제네릭과 가변인수를 함께 쓸 때는 신중하라

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

이펙티브 자바 - 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 매개변수를 담는 제네릭 배열이 만들어지는데, 이때 아래의 두 조건을 만족한다면 타입 안전하다고 말할 수 있다

  1. 메서드가 이 배열에 아무것도 저장하지 않는다.

  2. 그 배열의 참조가 밖으로 노출되지 않는다.(신뢰할 수 없는 코드가 배열에 접근할 수 없다.

제네릭 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)에 담아 리턴하는 안전한 방식을 취하자