[이펙티브 자바] 아이템32 | 제네릭과 가변인수를 함께 쓸 때는 신중하라

제롬·2022년 3월 28일
0

제네릭 가변인자 배열은 타입 안정성을 해칠수 있다.

가변인수: 자바 5 때 추가되었고 메서드에 넘기는 인수(매개변수)의 개수를 클라이언트가 동적으로 조절할 수 있게 해주는 기능이다.

[제네릭과 가변인사를 혼용하면 타입안정성을 해칠 수 있다.]

...
final List<Integer> integers = List.of(42);
Object[] objects = stringLists;
objects[0] = integers;				// 힙 오염 발생 
String s = stringLists[0].get(0);	// 형변환 예외(ClassCastException) 발생
...
  • 가변인수 메서드를 호출하면 가변인수를 담기 위한 배열이 자동으로 하나 만들어진다. 이렇게 만들어진 배열이 클라이언트에게 노출되는 문제가 발생한다.
warning: [unchecked] Possible heap pollution from 
parameterized vararg type List<String>
  • 매개변수에 제네릭이나 매개변수화 타입이 포함되면서 컴파일 경고가 발생하게 된다. 경고의 원인은 매개변수화 타입의 변수가 타입이 다른 객체를 참조하면서 힙 오염이 발생하기 때문이다.

  • 매개변수화 타입이 변수가 타입이 다른 객체를 참조하면 입 오염이 발생한다. 이렇게 다른 객체를 참조하는 상황에서는 컴파일러가 자동생성한 형변환이 실패하여 제네릭 타입 안정성이 보장되지 못한다.

제네릭 가변인자 매개변수의 모순점

  • 제네릭 배열을 프로그래머가 직접 생성하는건 허용하지 않으면서 제네릭 가변인자 매개변수를 받는 메서드를 선언할 수 있는건 분명 모순적이다.

  • 이런 모순을 수용한 이유는 제네릭이나 매개변수화 타입의 가변인자 매개변수를 받는 메서드가 실무에서 유용하기 때문이다.

  • 대표적인 예시로 Arrays.asList(T... a), Collections.addAll(Collection<? super T> c, T... elements), EnumSet.of(E first, E...rest)가 있다.

SafeVarargs

메서드 작성자가 그 메서드가 타입 안전함을 보장하는 장치로 자바7에서 추가되었다.

  • 매개변수 배열이 호출자로부터 순수하게 인수를 전달하는 일만 한다면 타입 안전하고 말 할수 있다. 하지만, 가변인자 매개변수에 아무것도 저장하지 않고도 타입 안정성을 깨는 경우가 있으니 조심해야 한다.

[제네릭 가변인자(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(b,c);
        case 2: return toArray(a,c);
    }
    throw new AssertionError(); // 도달 못함.
}

static <T> T[] toArray(T... args){
    return args;
}
...
public static void main(String[] args) {
    final String[] attributes = pickTwo("좋은", "빠른", "저렴한"); // ClassCastException 발생
}
  • pickTwo 메서드는 어떤 타입의 객체를 넘기더라도 담을 수 있는 가장 구체적인 타입의 Object타입의 배열을 반환한다.

  • 컴파일러가 String 배열로 형변환하는 코드를 자동으로 생성한다.

  • String 배열이 Object 배열의 하위 타입이 아니기 때문에 이 형변환은 ClassCastException이 발생하며 실패한다.

@SafeVarargs 사용 규칙

  • 제네릭이나 매개변수화 타입의 가변인자(varargs) 매개변수를 받는 모든 메서드에 @SafeVarargs를 달자.
  • 단, 타입 안전하지 않은 가변인자(varargs) 메서드에는 절대 사용해서는 안된다.

[가변인자 매개변수 배열에 다른 메서드가 접근하고도 안전한 예외경우]

@SafeVarargs
static <T>List<T>  flatten(List<? extends T>... lists){
   final List<T> result = new ArrayList<>();
        
    for (List<? extends T> list : lists){
        result.addAll(list);
    }

    return result;
}
  • @SafeVarargs 로 메서드의 타입 안전함을 보장하고 있다.

@SafeVarargs 사용 판단 기준

  • 가변인자(varargs) 매개변수 배열에 메서드에서 아무것도 저장하지 않는다.
  • 그 배열(혹은 복제본)을 신뢰할 수 없는 코드(클라이언트)에 노출하지 않는다.

@SafeVarargs 애너테이션은 재정의할 수 없는 메서드에만 달아야한다. 재정의한 메서드는 타입 안전성을 보장할 수 없기 때문이다. 자바 8에서는 정적 그리고 final 인스턴스 메서드에만 붙일 수 있었고, 자바 9 부터는 private 인스턴스 메서드에도 허용된다.

제네릭 가변인자 매개변수의 대안

가변인자(varargs) 매개변수를 List 매개변수로 바꿀수도 있다.

[제네릭 varargs 매개변수를 List로 대체한 경우 - 타입안전]

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(b,c);
        case 2: return List.of(a,c);
    }
    
    throw new AssertionError(); // 도달 못함.
}
...
public static void main(String[] args) {
    final List<String> attributes = pickTwo("좋은", "빠른", "저렴한");
}
  • List.of에도 @SafeVarargs 에너테이션이 달려있어 컴파일러가 메서드의 타입안전성을 검증하고 보장한다.
  • 배열없이 제네릭만 사용하므로 타입 안전하다.

정리

메서드에 제네릭 가변인자(varargs) 매개변수를 사용하고자 한다면 메서드의 타입 안전성을 확인하고 @SafeVarargs 에너테이션을 사용하자.

0개의 댓글