item 29에서 힙 오염에 대해 잠깐 언급했었다. 힙 오염은 매개변수화 타입의 변수가 타입이 다른 객체를 참조하면 발생한다
.
가변인수 메서드는 다음처럼 생겼다.
static <T> T[] toArray(T... args) {
return args;
}
여기에 제네릭을 적용해보면 힙 오염
이 발생할 수도 있다.
public static void method(List<String>... strs) {
List<Integer> intList = List.of(43);
Object[] objects = strs;
objects[0] = intList; // 힙 오염 발생
String s = strs[0].get(0); // ClassCastException
}
그럼 가변인수 메서드와 힙 오염이 어떻게 연관되어 있을까?
가변인수 메서드의 파라미터를 보면 List<String>...strs
의 List<string>을 담기 배열이 만들어진다. 앞서, 실체화 불가 타입(제네릭, 매개변수화 타입 등)은 타입 소거로 인해 런타임에 타입 정보가 없다고 배웠다. 그래서 런타임에 생성되는 이 배열은 List[]
이다! 런타임에는 타입 정보가 없으므로 어떤 타입도 우선 들어올 수가 있어진다.
이 메서드를 컴파일하면 비검사 경고를 보내는데 Possible heap pollution from ~
라고 나온다.
위의 가변인수 메서드 method를 보며 생각해보자.
런타임에는 타입 정보가 없다는 것을 이용하면, 컴파일만 통과하면 된다. Object[]로 strs가변인수 배열에 List<Integer>를 담을 수 있게 된다. 그러면 objects[0] = intList;
에서 힙오염이 발생하는데, 좀 더 자세히 말하면 strs가 힙 오염
된다고 본다.
이러한 비검사 경고를 없애기 위해서는 자바 7 이전에는 호출하는 곳마다 @SuppressWarnings("unchecked
")를 사용했다. 자바 7에서 @SafeVarargs가 추가된 이후로 번거로움을 없앨 수 있었다.
하지만, 경고를 없애는 행위이기 때문에 그 메서드가 타입 안전함
을 보장한 뒤에 사용해야 한다.
2가지 기준으로 타입 안전함을 보장할 수 있다.
varargs매개변수 배열에 아무것도 저장하지 않고 타입 안정성을 깰 수 있는데 확인해보자.
static <T> T[] toArray(T... args) {
return args;
}
처음 보여주었던 가변인수 메서드이다. 이 메서드가 반환하는 배열의 타입은 컴파일타임에 결정되는데, 그 시점에는 컴파일러에게 충분한 정보가 없어 타입을 잘 못 판단 할 수 있다.
이 내용은 아래 예제를 통해 좀 더 말해보겠다. 이 메서드를 보면 args를 반환함으로써 배열의 참조를 노출 하고 있다.
그럼 구체적으로 어떻게 문제가 되는지 보자.
public static void main(String[] args) {
String[] attributes = pickTwo("좋은", "빠른", "저렴한"); // ClassCastException
}
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();
}
이 상황에서 pickTwo메서드의 반환타입은 String[] 일까?
아니다. Object[]
이다. toArray메서드의 반환값을 그대로 반환하고 있으니, toArray를 보자.
컴파일 시점에서 toArray에 넘어오는 인수들은 T a, T b, T c로 이 타입을 String이 아닌 매개변수 타입 T이다. 아직은 컴파일러가 무슨 타입인지 정확히 모른다. 그러니까 toArray메서드 입장에서는 Object[]로 반환할 수 밖에 없다.
String[] array = toArray("좋은", "빠른", "저렴한"); System.out.println("array = " + array[0]);
이렇게 직접 toArray메서드 정확한 타입으로 넘겨주면 올바르게 동작한다. 컴파일러가 이제는 충분한 정보(String)을 받았기 때문이다.
그래서 pickTwo메서드의 반환 타입은 항상 Object[] 타입 배열이다. 그러니 main메서드에서 Object[] 을 String[]로 형변환하게 되니 ClassCastException이 발생하며 실패한다.
이러한 상황이 바로 varargs매개변수 배열을 올바르게 사용했지만 문제가 발생한 예이다.