[Effective Java] 아이템 32 : 제네릭과 가변인수를 함께 쓸 때는 신중하라
아이템 32 : 제네릭과 가변인수를 함께 쓸 때는 신중하라
가변인수
가변인수는 메서드에 넘기는 인수의 개수를 클라이언트가 조절할 수 있게 해줍니다. 덕분에 메서드를 n번 오버로딩 하지않고 원하는 개수만큼 인자를 넘길 수 있게 해줍니다.
이 가변인수 메서드를 호출하면 가변인수를 담기 위한 배열이 자동으로 하나 만들어지는데요. 아래와 같은 방법으로 확인할 수 있습니다.
public class Item32 {
public static void varargsMethod(String... args) {
System.out.println(Arrays.toString(args));
}
}
위와 같은 가변인수 메서드를 아래와 같은 코드로 호출해줍니다. 사실 호출하지 않아도 IDE 덕분에 타입이 유추가 되긴하죠..ㅎㅎ
public class TestApplication {
public static void main(String[] args) {
Item32.varargsMethod(null);
System.out.println("-----------");
Item32.varargsMethod("1");
System.out.println("-----------");
Item32.varargsMethod("1", "2", "3");
}
}
애플리케이션을 실행하면 결과는 아래와 같습니다. 한 개의 인자만을 전달하더라도 배열이 만들어집니다.
null
-----------
[1]
-----------
[1, 2, 3]
가변인수의 Heap Pollution
이렇게 편리한 가변인수에는 헛점이 있습니다. 바로 Heap Pollution을 유발할 수 있다는 것인데요.
가변인수 메서드 호출 시 자동으로 생성되는 이 배열이 메서드 내부에 노출이 되기 때문입니다.
컴파일 경고
가변인자가 제네릭이나 매개변수화 타입이 포함되면 알기 어려운 컴파일 경고가 발생합니다.
public class Item32 {
public static void varargsMethod(String... args) {
System.out.println(Arrays.toString(args));
}
public static void varargsMethodWithParameterizedType(List<String>... args) {
System.out.println("매개변수화 타입");
}
public static <T> void varargsMethodWithGeneric(T... args) {
System.out.println("제네릭");
}
}
이 클래스를 javac 명령어로 직접 컴파일해보겠습니다. (IntelliJ는 그냥 컴파일이 되어버리네요^^; 옵션이 있을것 같습니다.)
javac Item32.java
컴파일은 성공했으나 아래와 같이 메시지가 출력됩니다.
Note: Item32.java uses unchecked or unsafe operations.
Note: Recompile with -Xlint:unchecked for details.
-Xlint 옵션을 추가해서 다시 컴파일 해보도록 하겠습니다.
javac Item32.java -Xlint
컴파일 결과 아래와 같이 경고 메시지가 출력됩니다. 내용은 말 그대로 "Heap Pollution이 발생할 수 있다."입니다.
Item32.java:12: warning: [unchecked] Possible heap pollution from parameterized vararg type List<String>
public static void varargsMethodWithParameterizedType(List<String>... args) {
^
Item32.java:16: warning: [unchecked] Possible heap pollution from parameterized vararg type T
public static <T> void varargsMethodWithGeneric(T... args) {
^
where T is a type-variable:
T extends Object declared in method <T>varargsMethodWithGeneric(T...)
2 warnings
그렇다면 Heap Pollution은 어떻게 발생할까요?
Heap Pollution 의 발생과정
매개변수화 타입의 변수가 타입이 다른 객체를 참조하면 Heap Pollution이 발생합니다.
예시 코드를 통해 이 조건을 이해해보도록 하겠습니다.
public static void varargsMethodWithParameterizedType(List<String>... args) {
Object[] objects = args;
objects[0] = List.of(1, 2, 3); // Heap Pollution 발생
String value = args[0].get(0); // 가변인수 배열에서 첫번째 요소의 첫번째 요소 취득 -> ClassCastException 발생!!!
}
이 코드에서는 형변환을 하는 코드가 없음에도 ClassCastException이 발생하는데, 마지막 라인에 컴파일러가 생성한 형변환이 숨어있기 때문입니다. 무슨 말이냐구요? 이 코드의 컴파일 결과물을 보시면 바로 이해가 됩니다.
public static void varargsMethodWithParameterizedType(List<String>... args) {
args[0] = List.of(1, 2, 3);
String value = (String)args[0].get(0);
}
이처럼 제네릭 가변인수 배열에 값을 저장하는 것은 안전하지 않습니다.
그렇다면 왜 제네릭 가변인수 메서드를 선언할 수 있게 했을까요?
제네릭이나 매개변수화 타입의 가변인수 메서드가 실무에서 매우 유용하기 때문입니다. 자바 라이브러리에서도 이런 메서드를 이미 여럿 제공하고있는데, 다행히도 type-safe합니다.
자바 7 이전에는 제네릭 가변인수 메서드의 작성자가 호출자 쪽에서 발생하는 경고에 대해서 해줄 수 있는 일이 없었습니다.
그래서 호출자는 경고를 그냥 두거나 호출하는 위치마다 @SuppressWarnings("unchecked") 애너테이션을 추가해야했습니다.
그리고 애너테이션을 추가할 경우 진짜 문제를 알려주는 경고도 숨겨지기 때문에 좋지 않은 결과로 이어졌습니다.
자바 7에서는 @SafeVarargs 애너테이션이 추가되어 제네릭 가변인수 메서드 작성자가 호출자 쪽에서 발생하는 경고를 숨길 수 있게 되었습니다. @SafeVarargs 애너테이션은 메서드 작성자가 그 메서드가 type-safe 함을 보장하는 장치입니다. 컴파일러는 이 약속을 믿고 그 메서드가 안전하지 않을 수 있다는 경고를 더 이상 하지 않습니다.
때문에 메서드가 안전한 게 확실하지 않다면 절대 @SafeVarargs 애너테이션을 달아서는 안됩니다.
Type-safe 한 메서드의 조건
메서드가 가변인수 배열에 아무것도 저장하지 않아야합니다.
가변인수 배열의 참조가 밖으로 노출되지 않아야합니다.
static <T> T[] toArray(T... args) {
return args;
}
위와 같은 코드는 참조가 밖으로 노출되기 때문에 안전하지 않습니다.
가변인수 배열이 호출자로부터 그 메서드로 순수하게 인수들을 전달하는 일만 한다면 그 메서드는 안전합니다.
@SafeVarargs
- @SafeVarargs 애너테이션은 재정의 할 수 없는 메서드에만 달아야합니다. 재정의한 메서드에서 type-safe한지 보장할 수 없기 때문입니다.
- 자바 8에서 이 애너테이션은 정적 메서드와 final 인스턴스 메서드에만 붙일 수 있습니다.
- 자바 9부터는 private 인스턴스 메서드에도 허용됩니다.