Contents
Item32. 제네릭과 가변인수를 함께 쓸 때는 신중하라.
   Jan 30, 2023     8 min read

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

  • 핵심 정리
    • 제네릭 가변인수 배열에 값을 저장하는 것은 안전하지 않다.
      • 힙 오염이 발생할 수 있다.(컴파일 경고 발생)
      • Java 7에 추가된 @SafeVarargs 애노테이션을 사용할 수 있다.
    • 제네릭 가변인수 배열의 참조를 밖으로 노출하면 힙 오염을 전달할 수 있다.
      • 예외적으로, @SafeVarages를 사용한 메서드에 넘기는 것은 안전하다.
      • 예외적으로, 배열의 일부 함수를 호출하는 일반 메서드로 넘기는 것은 안전하다.
    • 아이템 28의 조언에 따라 가변인수를 List를 바꾼다면
      • 배열없이 제네릭만 사용하므로 컴파일러가 타입 안정성을 보장할 수 있다.
      • @SafeVarages 애너테이션을 사용할 필요가 없다.
      • 실수로 안전하다고 판단할 걱정도 없다.

⚓️서론

가변인수(varargs) 메서드와 제네릭은 자바 5때 함께 추가되었다.

서로 잘 어울리겠지? 라는 기대는 산산히 조각난다.

가변 인수는 메서드에 넘기는 인수의 개수를 클라이언트가 조절할 수 있게 해주는데, 구현 방식에 허점이 있다.

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

그 결과 varagrs 매개변수에 제네릭이나 매개 변수화 타입이 포함되면 알기 어려운 컴파일 경고가 발생한다.

이제 자세히 알아보자.


✅제네릭과 varargs를 혼용하면 타입 안전성이 깨진다.

public class Dangerous {
    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
    }

    public static void main(String[] args) {
        dangerous(List.of("There be drangous!")); // 6
    }

}
  • 매개변수화 타입(List… )의 변수가 타입이 다른 객체를 참조하면 힙 오염이 발생한다.

Untitled

🔡코드를 뜯어 보면

  1. 제네릭에 varargs 배열 매개 변수에 값을 저장하는 것을 볼 수 있다.
  2. Integer타입 리스트를 만들고 값(42)를 넣어줬다.
  3. stringLists(List 배열)를 Object[ ]에 할당해준다. 이유는 배열을 공변이기 때문에 가능 , 즉 String 타입을 Object 타입으로 사용하겠다는 뜻
  4. intList를 리스트 배열인 Object[0]에 넣어준다.
  5. stringLists[0].get(0) 해서 String s에 넣어준다. 따라서 ClassCastException 이 발생한다.

Untitled 1

제네릭을 쓰는 큰 특징은 컴파일 타임부터 런타임까지 타입 안정성을 확보하기 위한 용도로 쓰인다.

하지만 위의 코드는 런타임의 타입 안정성이 깨지는 모습을 보여준다.

그럼 안전하게 쓰는 방법은 ??


✅제네릭 varargs 매개변수를 안전하게 사용하는 메서드

  • 안전하게 쓰는 방법 예제
public class FlattenWithVarages {

    @SafeVarargs // 가변인자는 안전하게 사용되고 잇다는 것을 컴파일에게 알려준다. @SuppressWarnings의 구체적인 느낌
    //@SuppressWarnings("unchecked") // -> 이걸 쓰면 문제는 메소드 전체(모든 코드)가 SuppresWarnings이 되기 때문이다. 범위가 넓음
    static <T> List<T> flatten(List<? extends T>... lists) { //1
        List<T> result = new ArrayList<>(); //2
        for (List<? extends T> list : lists) //3
            result.addAll(list);
        return result; // 5

    }
    public static void main(String[] args) {
        List<Integer> flatList = flatten(List.of(1, 2), List.of(3, 4, 5), List.of(6, 7));
        System.out.println(flatList);

    }
}

자바 7 전에는 제네릭 가변인수 메서드의 작성자가 호출자 쪽에서 발생하는 경고에 대해서 해줄 수 있는 일이 없다.

사용하기에 신경이 쓰인다.

사용자는 이 경고들을 그냥 두거나 (더 흔하게는) 호출하는 곳마다 @SuppressWarnings(”unchecked”) 애너테이션을 달아 경고를 숨겨서 사용했다.

이런 과정은 안좋은 결과( 가독성 떨어지고, 진짜 문제를 알려주는 경고마저 숨김)로 이어진다.

자바 7에서는 @SafeVarargs 애너테이션이 추가되어 제네릭 가변인수 메서드 작성자가 클라이언트 측에서 발생하는 경고를 숨길 수 있게 되었다.

@SafeVarargs - 메서드 작성자가 그 메서드가 타입 안전함을 보장하는 장치

  • @SafeVarargs 사용 시 주의 사항
    • 메서드가 안전한 게 확실하지 않으면 절대 쓰지말라!

그렇다면 메서드가 안전한지 안한지 어떻게 알 수 있을까?

  • 조건 1.
    • 가변인수 메서드는 호출될 때 마다 vararsg 매개변수를 담는 제네릭 배열이 만들어지는데, 메서드가 이 배열에 아무것도 저장하지 말아야한다.
  • 조건 2.
    • 배열의 참조가 밖으로 노출되지 않아야 된다.
  • 예제 코드를 통해서 알아보자.
public class PickTwo {

    //코드 32-2 자신의 제네릭 매개변수 배열의 참조를 노출한다. -안전하지 않다(p.193)
    static <T> T[] toArray(T... args) { //1
        return args;
    }

    static <T> T[] pickTwo(T a, T b, T c) { // 2
        switch (ThreadLocalRandom.current().nextInt(3)) {
            case 0 : return toArray(a, b); //3
            case 1 : return toArray(a, b);
            case 2 : return toArray(a, b);
        }
        throw new AssertionError();
    }

    public static void main(String[] args) {
        String[] attributes = pickTwo("좋은", "빠른", "저렴한"); // 4
        System.out.println(Arrays.toString(attributes)); // 5

    }
}

🔡코드를 뜯어보자.

  1. 제네릭 배열을 return 했다. 즉 밖으로 노출했다.
 static <T> T[] toArray(T... args) {
        return args; //제네릭 배열을 return args-> 노출된다.
    }
  1. pickTwo를 호출하면 toArray가 호출되면서 제네릭 배열(args)이 노출된다. 즉, pickTwo를 쓰는 것이 toArray를 쓰는것과 동일하다.
    static <T> T[] pickTwo(T a, T b, T c) { // 2
        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();
    }
  1. 이 부분에서 toArray를 쓸 때, 컴파일러는 toArray의 리턴타입을 Object[]로 판단한다.

이유는 Object가 가상 추상적이기 때문에 어떤 타입이 오더라도 넣어 줄 수 있기 때문이다.

return toArray(a, b);
  1. 이 코드 컴파일 타임에서 Object[]에서 String[]으로 캐스팅한다.
String[] attributes = pickTwo("좋은", "빠른", "저렴한"); // 4

Untitled 2

  1. 결과는 ClassCastException 나온다.

이유는 Object[] 에서 구체적인 타입인 String 으로는 타입 캐스팅은 되질 않는다. (Runtime Exception)

System.out.println(Arrays.toString(attributes)); // 5

결과의 근본적인 문제를 살펴보면 내부적으로 만든 배열을 밖으로 노출되었기 때문이다.

자신의 제네릭 매개변수 배열의 참조를 노출했기 때문이다.


그럼 위 코드를 어떻게 안전하게 쓸 수 있을까 ?

간단히 말하자면 배열 대신 리스트를 사용하면된다.

배열 대신 리스트를 만드는방법 자세히 보려면 item28

  • pickTwo를 안전하게 사용한 예제
public class SafePickTwo {

    static <T> List<T> pickTwo(T a, T b, T c) {
        switch (ThreadLocalRandom.current().nextInt(3)) {
            case 0 : return List.of(a, b); // 1
            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("좋은", "빠른", "저렴한");
        System.out.println(attributes);

    }
}

🔡코드를 뜯어보자

  1. 원래 코드(pickTwo) 가변인수(T…) 대신에 List.of를 사용하면된다.
List.of(a, b);
List.of(a, c);
List.of(b, c);

List를 사용하여 타입 안정성이 보장이된다.

  • Flatten를 안전하게 사용한 예제
public class FlattenWithList {

    static <T> List<T> flatten(List<List<? extends T>> lists) { // 1
        List<T> result = new ArrayList<>();
        for (List<? extends T> list : lists)
            result.addAll(list);
        return result;
    }

    public static void main(String[] args) {
        List<Integer> flatList = flatten(List.of(List.of(1, 2), List.of(3, 4, 5), List.of(6, 7)));
        System.out.println(flatList);

    }
}

🔡코드를 뜯어보자

  1. 원래 코드(FlattenWithVarages) 에서 안전하게 사용할 때
List<? extends T>... -->  List<List<? extends T>>