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

제너릭 가변인수?

public static void main(String[] args){
    String[] strArr1 = new String[]{"a", "b", "c"};
    String[] strArr2 = new String[]{"d", "e", "f"};
    List<String[]> strList = Arrays.asList(strArr1, strArr2);
    int[][] intArr = {{1, 2, 3, 4}, {5, 6, 7}};
    List<int[]> intList = Arrays.asList(intArr);

    System.out.println("------------------");
    strList.forEach(element-> System.out.println(Arrays.toString(element)));
    System.out.println("------------------");
    intList.forEach(element-> System.out.println(Arrays.toString(element)));
}

// 자바 API - Array를 List로 변환
public static <T> List<T> asList(T... a) {
    return new ArrayList<>(a);
}

요약

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

  • 배열과 제네릭의 타입 규칙이 서로 다르다.

  • 특징: 배열 vs 제너릭arrow-up-right

    • 배열 - 런타임에 실체화

    • 제너릭 - 런타임에 소거 (ArrayList이 ArrayList가 됨)

      • 컴파일 시점 타입을 안전하게 검사해줌

  • 이러한 특징으로 제너릭 배열 사용시 단점이 존재한다.

  • 그럼에도 제너릭 가변인수가 존재하는 이유는? 유용하기때문

타입 규칙의 차이: 배열 vs 제네릭

  • 배열은 공변 (부모타입으로 치환 가능) 이다.

  • 제네릭은 불공변이다.

이러한 타입 규칙의 차이는, 제네릭이 주는 컴파일 시점 타입 에러를 잡을 수 있는 타입 안정성이라는 근간을 흔들 수 있다.

아래는 제너릭 가변인수를 사용한 예시인데, stringLists를 인수로 받을때 Ojbect[]로 컴파일러가 자동으로 변환해준다. 이 스트링 리스트를 objects에 담고 int를 넣어주어 힙을 오염 시켰는데, 컴파일 시점에 에러가 나지 않고 런타임에 ClassCastException 이 발생했다.

이처럼 타입 안정성을 깨뜨리는데, 왜 제너릭 가변인수를 만들었을까??

퀴즈

다음 코드는 제너릭 가변인수 사용시 추상화가 완벽하지 못함을 보여주는 코드이다.

이 코드에 ClassCastException 이 나는 이유는 무엇일까?

이유 - 부모타입 Object[] 을 자식타입 String[] 으로 형변환이 불가하기에.

자세히 설명하면, toArray() 가 호출될때 new Object[]{"좋은","빠른"} 처럼 Object 배열을 만들어 넘겨진다 (String이 아닌 Object로 변환되는 이유는, pickTwo에 어떤 타입의 객체를 넘기더라도 담을 수 있는 유연한 타입이기 때문이다 ). 그리고 toArray 메서드 안에서는 args를 바로 넘겨버리는 전달 역할만 하기에 결국 attributes 변수에 받게되는 값는 Object 배열인 것이다. Object를 String 으로 형변환 시도를 했기에 에러가 발생한 것이다.

그럼에도 제네릭 가변인수를 사용하는 이유

  • 제네릭 varargs 매개변수는 타입 안전하지는 않지만, 실무에서 유용하기 때문에 허용된다.

  • 자바 API 중 제네릭 가변인수 예시

    • Arrays.asList(T... a)

    • Collections.addAll(Collections<? super T> c, T... elements)

    • EnumSet.of(E first, E... rest)

제너릭 가변인수 안전하게 사용하기

  • 제너릭 가변인수를 타입 안전하게 사용하는 2가지 방법

    • 가변인수 배열에 아무것도 저장하지 않는다.

      • 1번째 예시 처럼 dangerous(List<String>... stringLists) 의 인수 stringLists에 값을 저장하지 말고

    • 그 배열(혹은 복제본)을 신뢰할 수 없는 코드에 노출하지 않는다.

      • 2번째 예시 처럼 toArray(T... args) 처럼 그대로 노출하지 말자.

  • 실체화 불가 타입으로 가변인수를 선언하면 컴파일러가 경고를 보내는 문제 해결 방법

제너릭 가변인수 작성시 이런식으로 작성해줘야 한다.

아래 코드는 위 퀴즈에서 본 toArray 대안이다.

Last updated