#32 제네릭과 가변 인수를 함께 쓸 때는 신중하라
"제네릭과 가변 인수를 함께 쓸 때는 신중하라"는 결국 "제네릭과 가변 인수(배열)를 함께 쓸 때는 신중하라"라는 의미를 나타냅니다.
위 코드에서 보듯이, 자바에서는 기본적으로 제네릭 배열(T[]) 직접 생성을 허용하지 않습니다.
Super 클래스 - Sub 클래스 (상속 관계)
Super[] - Sub[] (상속 관계) // 공변
List<Super> - List<Sub> (아무 관계 아님) // 불공변
////////////////////////////////////////////////////////////////////////////////////////////////
Object[] objects = new Long[1];
objects[0] = "g"; // 런타임 에러(ArrayStoreException) 발생, 구체적(reified)
List[] objectList = new ArrayList<Long>();
objectList.add("g"); // 컴파일 에러 발생, 비구체적
////////////////////////////////////////////////////////////////////////////////////////////////
T[] newArray = new T[SIZE]; // 타입이 안전하지 않기 때문에 직접 제네릭 배열 생성 허용 X
** 구체적(reified) : 런타임 시에 자신의 요소 타입을 체크한다는 의미입니다.
하지만 가변 인수를 통해 제네릭 배열을 이용할 수 있습니다.
static <T> void gv(T... args) { // 가변 인수 args를 담기 위한 배열이 자동으로 만들어짐
T[] varargsArray = args; // ok
}
가변 인수를 통해 제네릭 배열을 이용하면 어떠한 문제가 발생하는지에 대해 알아보겠습니다.
제네릭과 가변 인수를 같이 쓰면 힙 오염(Heap pollution)이 발생할 수 있습니다.
** 힙 오염(Heap pollution) : 매개변수화 타입의 변수가 타입이 다른 객체를 참조하는 경우 발생.
왜 힙 오염이라 표현하는지 찾아보려 했는데, 못 찾겠네요ㅠ
배열이 힙 영역에 저장되고, 의도와 달리 오염? 되기 때문에 힙 오염이라 표현하지 않나 추측합니다.
위 코드처럼 런타임 시 예외가 발생할 수 있기 때문에, 가변 인수 매개변수 배열에 값을 저장하는 것은 안전하지 않습니다.
런타임 시 예외가 발생할 수 있기 때문에, 가변 인수 매개변수 배열을 그대로 노출하는 것도 안전하지 않습니다.
제네릭 배열을 직접 생성은 못하게 하면서,
제네릭 가변 인수 매개변수를 통한 생성을 허용한 이유는?
실무에서 유용해서..
힙 오염이 발생할 수 있고, 런타임 시 예외가 발생할 수 있는데도 가변 인수를 통해 제네릭 배열을 생성하게 한 이유는 책에 따르면, 실무에서 유용해서 라고 하네요.
이용하는 개발자들이 주의해야 합니다.
실제 자바 라이브러리에서도 이런 메서드를 여럿 제공하고 있습니다.
Collections.addAll(Collections<? super T> c, T... elements)
EnumSet.of(E first, E... rest)
자바 라이브러리에서 제공하는 제네릭 타입의 가변 인수는 타입 안정성을 보장하고 있습니다.
이런 경우에는 @SafeVarargs annotation을 메서드에 추가하면, 더 이상 이 메서드를 호출하는 부분에서 힙 오염 경고를 표시하지 않습니다.
반대로 말하면 타입 안정성을 보장할 수 없다면, @SafeVarargs를 써서는 안 됩니다.
타입 안정성을 보장하기 위해서는 몇 가지 조건이 필요합니다.
제네릭 가변 인수로 인해 생성된 배열에 아무것도 저장하지 않는다.
생성된 배열(or 복제본)의 참조가 밖으로 노출되지 않는다.
제네릭 가변 인수로 순수하게 인수들을 전달받기만 한다.
@SafeVarargs
public static <T> List<T> asList(T... a) {
// 제네릭 가변 인수로 생성된 배열(T[] ts = a;)에 아무것도 저장하지 않는다.
// 생성된 배열의 참조 아닌, new ArrayList<>(a)가 밖으로 노출된다.
// 제네릭 가변 인수로 순수하게 인수들을 전달받기만 한다.
return new ArrayList<>(a);
}
반드시 배열을 써야 할 상황이 아니라면 List를 이용할 수 있습니다.
위에서 봤던 toArray(T... args) 메서드 대신, 자바 라이브러리 차원에서 제공하는 List.of(E... elements)를 이용할 수도 있습니다.
https://docs.oracle.com/javase/tutorial/java/generics/nonReifiableVarargsType.html
http://www.angelikalanger.com/GenericsFAQ/FAQSections/ProgrammingIdioms.html#FAQ300
힙 오염 관련 내용이 많지 않네요.
제네릭 가변 인수를 쓰는 경우가 많진 않지만, 쓴다면 고민할 부분이 있는 거 같습니다.