ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Java] 제네릭과 가변인수 (Effective Java 3rd_item32)
    Java 2019. 8. 18. 13:39
    반응형

     가변인수(varargs) 메서드와 제네릭은 Java 5 때 추가되었습니다. 가변인수 메서드는 이름 그대로 메서드의 인수를 클라이언트가 조절하여 호출할 수 있는 메서드 입니다. 간단한 예시로 아래와 같은 코드가 있습니다.

        public static void printVarargsString(String... strings) {
            System.out.println(strings.getClass().getSimpleName());
            System.out.println();
            for (String string : strings) {
                System.out.println(string);
            }
        }
    
        public static void main(String[] args) {
            printVarargsString("a", "b", "c");
        }

    메인 함수를 실행하여 출력된 결과

     가변인수 메서드를 호출하면 위와 같이 가변인수를 담기위한 배열이 자동으로 만들어집니다. 하지만 이러한 가변인수 매개변수에 제네릭이나 매개변수화 타입이 포함되면 컴파일 경고가 발생합니다. 이는 제네릭이나 매개변수화 타입이 실체화가 불가능한 타입이기 때문입니다. 매개변수화 타입의 변수가 다른 타입의 객체를 참조하면 힙 오염이 발생합니다.

     

     Effective Java 3rd 에 나오는 예제 코드를 보겠습니다.

        static void dangerous(List<String>... stringLists) {
            List<Integer> integers = Collections.singletonList(41);
            Object[] objects = stringLists;
            objects[0] = integers;              //힙 오염 발생
            String s = stringLists[0].get(0);   //ClassCastException
        }
        
        //Parent Class - Child Class (Parent 클래스를 상속)
        //Parent[] parents - Child[] children (상속 관계 - 공변)
        //List<Parent> parents - List<Child> children (상속 관계가 아님 - 불공변)

     이 메서드는 형변환하는 곳이 보이지 않지만, 마지막 줄에서 컴파일러가 생성한 형변환이 숨어 있기 때문에 ClassCastException이 발생합니다. 이 처럼 타입 안전성이 깨지게 되므로 제네릭 가변인수 매개변수에 값을 저장하는 것은 안전하지 않습니다.

     

     

    아래와 같은 코드도 자신의 제네릭 매개변수 배열의 참조를 노출하므로 안전하지 않습니다.

        /* 입력받은 제네릭 매개변수들을 배열로 감싸주어 유용한 유틸리티 메서드로 보이지만, 안전하지 않다. */
        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(b, c);
                case 2: return toArray(a, c);
            }
            throw new AssertionError(); //도달할 수 없다.
        }

     위의 코드는 컴파일러가 pickTwo 메서드에서 toArray 메서드로 넘길 T 인스턴스 2개를 담을 varargs 매개변수 배열을 만드는 코드를 생성합니다. 이 코드가 만드는 배열의 타입은 pickTwo 메서드에 어떤 타입의 객체를 넘기더라고 담을 수 있는 가장 구체적인 타입인 Object[] 입니다. 그리고 toArray 메서드가 반환한 Object[] 타입이 pickTwo() 메서드를 호출한 클라이언트까지 전달됩니다. 즉, pickTwo() 메서드는 항상 Object[] 타입의 배열을 반환합니다.

     

     이러한 문제는 아래와 같은 코드에서 ClassCastException 에러를 던집니다.

        public static void main(String[] args) {
            String[] attributes = pickTwo("a", "b", "c");
        }

    pickTwo() 메서드는 Object[] 배열을 리턴하기 때문에 String[] 으로의 캐스팅에 실패하기 때문입니다.

     


     

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

     위의 예제는 제네릭 타입의 가변인수 매개변수를 안전하게 사용하는 예입니다.

    @SafeVarargs 애너테이션은  메서드 작성자가 그 메서드가 타입 안전함을 보장하는 장치입니다. 컴파일러는 이 약속을 믿고 그 메서드가 안전하지 않을 수 있다는 경고를 발생시키지 않습니다.

     

    @SafeVarargs 애너테이션 사용 규칙

    • varargs 매개변수 배열에 아무것도 저장하지 않는다. (그 매개변수들을 덮어쓰지 않는다.)
    • varargs 매개변수 배열의 참조가 신뢰할 수 없는 코드에 노출되지 않도록 한다.

     

     @SafeVarargs 애너테이션 타입의 안전성을 보장하면서 varargs 매개변수를 배열로 받는 방법 말고도, 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;
        }

     


    참고 자료

    Effective Java 3rd (인사이트 출판)

    반응형

    댓글

Designed by Tistory.