Item32. Generic과 가변인수를 함께 쓸떄는 신중하라

2022-01-04 19:51:50

#Java#Effective Java 3/E

가변인수에 Generic은 타입 안전하지 않다.

가변인수(varargs) method와 Generic은 JDK 5때 함께 추가되었다.

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

가변인수 type을 실체화 불가 타입으로 선언하면 heap pollution 이 생길 수도 있다는 compile 경고가 뜬다.

아래와 같은 상황에서 heap pollution이 발생할수 있기 떄문이다.

    static void dangerous(List<String> ... stringList){
//      List<String>[] stringList = stringList;
        Object[] objects = stringList;
        List<Integer> integers = List.of(42);
        objects[0] = integers;
        String s = stringList[0].get(0);
    }

매개 가변변수 stringList는 Generic 배열타입이 되고, 배열은 공변이기 떄문에 Object[] 타입으로 받을 수 있다. 따라서 Object 배열에 다른 타입매개변수를 갖는 Generic을 넣는 경우, 값을 꺼낼떄 문제가 생긴다.

@SafeVaraagrs

JDK 7에서 부터 추가되어, Method 작성자가 해당 Method가 타입안전함을 보장해주는 기능을 하며, Generic 가변인수 method 작성자가 client측에 발생하는 경고를 숨길 수 있게 해준다.

Generic 가변인수 method는 다음과 같을떄 타입안전하다고 한다.

  • Generic 배열에 아무것도 저장되지 않으면서, 배열의 참조가 외부로 노출되지 않는 경우

다음과 같이 배열의 참조를 반환하는 경우 타입안전하지 않다.

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();
}

3개의 parameter를 넘겨주고 그중 무작위로 선택된 2개의 배열을 반환받는 method가 있다고 가정했을떄 직접 실행해보면 runtime에 class cast exception이 터진다.

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

그이유는 toArray(T..args) method에서 내부적으로 parameter를 받을 Object[] 배열을 만드는 코드를 생성하기 때문이다. 반환된 Object 배열은 client 측에서 compiler에 의해 String 배열로 자동 형변환된다. 이떄 Object[]는 String[] 의 하위타입이 아니기 떄문에 형변환은 실패한다.

예외

  1. @SafeVarargs로 제대로 annotation된 또 다른 varargs method에 Generic 배열을 넘기는 건 안전하다.

  2. Generic 배열 내용의 일부 함수를 호출만 하는 일반 method에 넘기는 것도 안전하다.

다음은 Generic 가변인수 매개변수를 안전하게 사용하는 예제이다.

Generic 배열의 참조값을 반환하지도, Generic 배열에 값을 삼입하지도 않고 있다.

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

@SafeVarargs 작성 규칙

  • Generic이나 매개변수화 타입의 varargs 매개변수를 받는 모든 method에 @SafeVarargs를 달아야 사용자를 헷갈리게 하는 compiler 경고를 제거할 수 있다.
  • @SafeVarargs annotation을 달기전에 타입 안전한지 (heap pollution은 없는지) 확인하고, 달아야 한다.

추가로 해당 method를 overriding시에도 타입 안전한지는 보장할수 없으므로, JDK 8에서부터는 overriding 불가능한 static method와 final method에만 붙일 수 있다. JDK9 부터는 private method에도 허용된다.

가변인수 대신 List 사용

Generic 가변인수를 받는 method가 있을떄 타입안전한지 확인하고, overriding할수 없는 상태인지 확인하고 , @SafeVarargs를 붙여주는 방법도 있겠지만 그냥 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;
}

client에서 flatten method에 값을 전달할떄는 List.of(...) method를 사용하면 된다.

이 방식의 장점은 개발자가 실수로 타입 안전하다고 잘못판단할 가능성이 없으며 , 단점은 client코드가 조금 지저분해진다는 단점을 가지고 있다.

또한 이방식은 이전 예제코드인 타입 불안전한 toArray method를 사용하지 않고, 우회할떄에도 사용가능하다.

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();
}

이를 List.of(...)로 타입안전하게 변경하면 다음과 같이 변경할 수 있다.

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(a,c);
        case 2: return List.of(b,c);
    }
    throw new AssertionError();
}

정리

가변인수에 Generic을 넣으면 내부적으로 Generic 배열이 생성되어 heap pollution이 발생하는 등 타입 안전하지 않을수도 있다. 따라서 Generic 배열의 참조를 반환하지 않고, 값도 넣지 않음을 확인하고 @SafeVarargs 을 붙이던지, 가변인수 자체를 제거하고 List로 변경하자.

프로필 이미지
@chani
바둑 좋아하는 개발자의 의미있는 학습 기록을 위한 공간입니다.

댓글

이 게시글에 대한 의견을 공유해주세요!

댓글