본문 바로가기

Java

[Effective Java] 아이템32. 제네릭과 가변인수를 함께 쓸 때는 신중하라

1. What: 제네릭에 가변인수를 쓰는 예시

2. Why: 가변인수와 제너릭을 함께쓰면 타입안정성이 보장되지 않는다.

static void dangerous(List<String>... stringLists) { // (1)
  List<Integer> intList = List.of(42); // (2)
  Object[] objects = stringLists; // (3)
  objects[0] = intList; // (4) 힙 오염 발생
  String s = stringLists[0].get(0); // (5) ClassCastException
}

가변인수 메서드를 호출하면 가변인수를 담기위한 배열이 자동으로 하나 만들어진다.

-> 위 예시에서는 List<String>[] stringLists이다.// (1)

이 배열이 Obejct[]에 저장될 수 있고 // (3) 

objects[0]에 다른 타입인 intList가 들어간다. // (4) 

마지막 줄에서 stringLists의 0번째 객체를 호출하면 ClassCastException이 발생한다. //(5)

-> 위 처럼 타입 안정성이 깨지기 때문에 제네릭에 가변인수를 쓰는 것은 안전하지 않다.

 

 

그렇다면 왜 허용하는걸까??

- 제네릭이나 매개변수화 타입(ex:List<String>)의 varagrs 매개변수를 받는 메서드가 실무에서 유용하기 때문이다.

- 예시!! (위 예시와 다르게 아래예시들은 타입 안전하다)

1. Arrays.asList(T... a) -> 간단하게 원하는 원소들로 리스트를 만들 수 있으니 유용

2. Collections.addAll(Collection<? super T> c, T...elements)

3. EnumSet.of(E first, E... rest)

 

 

3. How: (1) 메서드가 타입 안전함을 보장 (2) 가변인수 대신에 List로 생성

1) @SafeVarargs로 메서드 작성자가 그 메서드가 타입 안전함을 보장한다. 

- 메서드 작성자가 메서드에 @SafeVarargs를 붙이면 클라이언트에게 경고문을 노출하지 않는다.

 

타입 안전한 경우

- 배열에 아무것도 저장하지 않고, 배열의 참조가 외부로 노출되지 않는 경우

- 인수를 전달하기만 하는 경우(가변인수의 목적)

 

 

ex) 타입 안전하지 않은 경우 - 제네릭 매개변수 배열의 참조를 노출한다.

static <T> T[] toArray(T... args) {
  return args;
}

반환 타입은 메서드에 인수를 넘기는 컴파일 타임에 결정되는데, 그 시점에는 컴파일러에게 충분한 정보가 주어지지 않아 타입을 잘못 판단할 수 있다.

때문에 잘못된 반환정보로 메서드를 호출한 쪽에 힙오염을 시킬 수 있다.

 

 

@SafeVarargs를 적용한 예시

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

1. varags 매개변수 배열에 아무것도 저장하지 않는다.

2. 그 배열(혹은 복사본)을 신뢰할 수 없는 코드에 노출시키지 않는다.

해당 메서드는 위 두 가지 조건을 만족하여 안전함을 보장한다. 안전한 varargs 메서드에는 @SafeVarargs 어노테이션을 달아서 컴파일러 경고를 없애는 것이 좋다.

 

 

2) 가변인수를 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.of를 활용하면 메서드에 임의의 갯수의 인수를 넘길 수 있다! (= 가변인수와 같은 역할을 수행한다.)

[장점]

직접 @SafeVarargs를 달지 않아도 된다. 즉 직접 타입 안정성을 검증할 필요가 없다. (List.of에  @SafeVarargs이 달려있다. = 컴파일러에서 타입안정성을 체크한다.)

[단점] 코드가 조금 지저분하고 속도가 약간 느려질 수 있다.