티스토리 뷰

반응형

가변인수와 제네릭

가변인수 메서드와 제네릭은 자바 5때 함께 추가되었지만 잘 어우러지지 않는다.

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

이를 내부로 감춰야했는데 클라이언트에 노출해버려서 제네릭이나 매개변수화 타입이 포함되면 컴파일 경고 발생

실체화 불가 타입(제네릭, 매개변수화 타입)은 런타임에는 컴파일타임보다 타입 관련 정보를 적게 가지고 있음

제네릭과 가변인수를 혼용하면 타입 안전성이 깨진다.

//제네릭 varargs 배열 매개변수에 값을 저장하는 것은 안전하지 않다.
public void print(List<String> ... stringList){
    List<Integer> intList = List.of(42);
    Object[] objects = stringList;
    objects[0] = intList;  //3 힙오염 발생(매개변수화 타입 변수가 타입이 다른 객체 참조해서)
    String s = stringList[0].get(0);   //4  ClassCastException
}

그런데, 왜 위 코드는 경고로 끝낼까? 제네릭이나 매개변수화 타입의 varargs 매개변수를 받는 메서드가 실무에서 매우 유용하기 때문에 모순을 수용하기로 한것.

ex) Arrays.asList(T... a), Collections.addAll(Collection<? super T> c)

 

 

@SafeVarargs 애너테이션

자바 7 전에는 제네릭 가변인수 메서드의 작성자가 호출자 쪽에서 발생하는 경고에 대해서 해줄수 있는게 없었고, 호출자는 경고들을 없애기 위해서 @SuppressWarnings(”unchecked”) 애너테이션을 다 달아줘야했다.

→ 자바7부터 @SafeVarargs 애너테이션이 추가되어 작성자가 이 애너테이션을 추가하면 호출자는 경고를 숨길 수 있게 되었다. (그 메서드가 타입 안전성을 보장할때만 애너테이션 달기)

제네릭 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(); // Can't get here
    }

    public static void main(String[] args) {
        //Object[]는 String[]의 하위타입이 아니므로 ClassCastException 발생
        String[] attributes = pickTwo("Good", "Fast", "Cheap");
        System.out.println(Arrays.toString(attributes));
    }
}

@SafeVarargs는 재정의할수 없는 메서드에만 달아라.

자바8에서는 정적 메서드와 final 인스턴스 메서드에만 붙일 수 있고,

자바9에서는 private 인스턴스 메서드에도 붙일 수 있다.

 

 

제네릭 varargs 매개변수를 안전하게 사용하는 법

제네릭이나 매개변수화 타입의 varargs 매개변수를 받는 모든 메서드에 @SafeVarargs를 달아라.

즉, 안전한 메서드만 작성해라

  • varargs 매개변수를 List 매개변수로 바꿀수도 있음
@SafeVarargs
static <T> List<T> flatten(List<? extends T>... lists) {
    //...
}

//위 메서드를 List 매개변수로 대체
static <T> List<T> flatten(List<List<? extends T>> lists) {
    //...
}

위의 pickTwo를 List 버전으로 바꾸면?

제네릭은 배열과 다르게 런타임 시점에 타입 정보를 소거하므로 캐스팅 에러가 나지 않음

static <T> List<T> pickTwo(T a, T b, T c) {
    System.out.println(a.getClass().getName());
    switch (ThreadLocalRandom.current().nextInt(3)) {
        case 0:
            return Arrays.asList(a, b);
        case 1:
            return Arrays.asList(b, c);
        case 2:
            return Arrays.asList(a, c);
    }
    throw new AssertionError();
}

public static void main(String[] args) {
        List<String> strings = pickTwo("좋은", "빠른", "저렴한");
}

 

 

결론

가변인수와 제네릭은 궁합이 좋지 않다.

  • 가변인수 기능은 배열을 노출하여 추상화가 완벽하지 못함
  • 배열과 제네릭의 타입 규칙이 서로 다름

제네릭 varargs 매개변수는 타입 안전하진 않지만 허용함 (유용하기 때문)

제네릭 varargs 매개변수를 사용하고 싶으면 타입 안전한지 확인한 후 @SafeVarargs 를 달아주도록 하자.

반응형
댓글
반응형
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday