이번챕터는 가변인수와 제네릭의 궁합에 대한 설명이다!

가변인수에 대해 제대로 정리하고 넘어가고 싶어서 가변인수부터 정리하겠다.

 

가변인수, 혹은 가변인자(varargs)는 여러개의 매개변수를 받을 수 있는 코드다.

기본적으로 타입... 변수의 형태로 사용하는데 오버로딩(overloading)을 생각하면 쉬운 형태이다.

class test {

    public static void show(String... names){
        for(String name : names){
            System.out.println("name = " + name);
        }
    }

    public static void main(String[] args) {
        show("홍길동", "김철수", "박영희");
    }

}

여기서 가변인자는 String... names를 의미하며 배열처럼 String을 받아 출력해주고 있다.

이렇게 하면

 

name = 홍길동

name = 김철수

name = 박영희

 

형태로 출력이 되며 show 메소드는 파라미터의 개수가 변해도 일정하게 출력해줄것이다.

 

그렇다면 제네릭과 사용할때 주의해야 할 점은 무엇일까

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

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

 

배열이 나왔다면 우리는 아이템 28에서 배열보다 리스트를 사용하라 했었던 챕터를 떠올릴 수 있다.

static void danferous(List<String>... stringLists) {
    List<Integer> intList = List.of(42);
    Object[] objects = stringLists;
    objects[0] = intList; // 힙 오염 발생
    String s = stringLists[0].get(0); // ClassCastException
}

이 메소드에서는 형변환하는 곳이 보이지 않는데도 인수를 건네 호출하면 ClassCastException을 던진다.

마지막 줄에 컴파일러가 생성한(보이지않은) 형변환이 숨어 있기 때문이다.

 

이처럼 타입 안전성이 깨지니 제네릭 varargs배열 매개변수에 값을 저장하는 것은 안전하지 않다.

 

그렇다면 메소드가 안전한지는 어떻게 확신해야 할까?

우리가 가변인수 메소드를 호출할 때 varargs 매개변수를 담는 제네릭 배열이 만들어진다는 사실을 기억하자.

메소드가 이 배열에 아무것도 저장하지 않고 그 배열의 참조가 밖으로 노출되지 않는다면 타입 안전하다.

 

이 varargs 매개변수 배열이 호출자로부터 그 메소드로 순수하게 인수들을 전달하는 일만 한다면

그 메소드는 안전하다는 말이다.

 

포인트는 두개이다.

  • varargs매개변수 배열에 아무것도 저장하지 않는다.
  • 그 배열을 신뢰할 수 없는 코드에 노출하지 않는다.

예시를 봐보자

 

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

flatten메소드는 임의 개수의 리스트를 인수로 받아, 받은 순서대로 그 안의 모든 원소를 하나의 리스트로 옮겨 담아 반환한다.

결론

가변인수와 제네릭은 궁합이 좋지 않다. 가변인수 기능은 배열을 노출하여 추상화가 완벽하지 못하고, 배열제네릭의 타입 규칙이 서로 다르기 때문이다. 메서드에 제네릭 (혹은 매개변수화된) varags 매개변수를 사용하고자 한다면, 먼저 그 메서드가 타입 안전한지 확인한 다음 @SafeVarargs 애너테이션을 달아 사용하는 데 불편함이 없게끔 하자

+ Recent posts