이펙티브 자바, 쉽게 정리하기 - item 32. 제네릭과 가변인수를 함께 쓸 때는 신중하라
가변인수 메서드의 허점
- 가변인수 메서드를 호출하면 가변인수를 담기 위한 배열이 자동으로 하나 만들어진다.
- 이 배열은 내부로 감춰져야 하는데, 클라이언트에 공개되면서 문제가 발생할 수 있다.
- 특히
varargs
변수에 제네릭이나 매개변수화 타입이 포함되면 알기 어려운 컴파일 경고가 발생한다.
- 특히
실체화 불가 타입(제네릭)을 가변인수로 이용했을 때
@Test
public void unableToReifyTest() {
Assertions.assertThrows(ClassCastException.class, () -> {
reifyExampleMethod(List.of("안","녕","하"));
});
}
public static void reifyExampleMethod(List<String>... stringLists) {
List<Integer> intList = List.of(42);
Object[] objects = stringLists;
objects[0] = intList;
String s = stringLists[0].get(0); // ClassCastException 발생
}
- 간단한 테스트코드로 작성해본 예시이다.
varargs
를 배열 매개변수 값에 저장하여 조작하거나, 외부로 노출하는 것은 안전하지 않으므로 금지하는 것이 좋다.
자바 표준 API에서의 제네릭 가변인수 활용
@SafeVarargs
@SuppressWarnings("varargs")
public static <T> List<T> asList(T... a) {
return new ArrayList<>(a);
}
- 위험한 행위만 하지 않는다면, 매우 편리한 도구이기 때문에 표준 API에서도 사용한다.
- 가변인수를 외부에 노출하는 행위는 위험하다.
- 다른 배열 값에 저장하여 조작하는 등의 행위는 위험하다.
- 위의 2가지 위험한 행위를 하지 않고 원칙을 잘 지켰다면,
@SafeVarargs
애너테이션을 통해 컴파일 경고를 지울 수 있다.
위험한 제네릭 가변인수 활용의 예
public static <T> T[] toArray(T... args) {
// 가변인자를 그대로 반환하는 것은 외부 메서드에서 사용돼선 안된다.
// 가변인자를 그대로 반환하여 외부에 노출하지 말자!
return args;
}
- 주석에 설명된 것처럼 위의 경우엔 가변인자를 그대로 외부에 노출해서 위험하다.
- 이 메서드가 반환하는 배열의 타입은 이 메서드에 인수를 넘기는 컴파일 타임에 결정되는데, 그 시점에 컴파일러에게 충분한 정보가 주어지지 않아 타입을 잘못 판단할 수 있다.
- 잘못된 타입을 그대로 반환하면, 힙 오염을 클라이언트의 콜스택까지 전달하는 결과를 낳을 수도 있다.
/**
* 가변인자 제네릭 인자를 반환하는 `toArray()`의 결과를 이용하기 때문에
* 항상 `Object[]` 타입을 반환한다.
* **중요**, 제네릭 varargs 배열에 다른 메서드가 접근하도록 허용하면 안전하지 않다.
* 제네릭 varargs 배열은 사용하는 해당 메서드에서만 접근하는 것이 좋을 것이다.
*/
public 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(); // 도달할 수 없다.
}
- 위 메서드는 가변 제네릭 인자를 반환한
toArray()
의 결과를 이용하기 때문에 위험하다.
@Test
public void test() {
String[] strings = toArray("일", "이", "삼");
System.out.println("strings = " + Arrays.toString(strings));
Assertions.assertThrows(ClassCastException.class, () -> {
// 만일, `String[] pickTwo`와 같이 코드를 작성하면,
// 여기서 컴파일러는 `String[]`로 `pickTwo()`의 결과를 캐스팅하려고 한다.
String[] pickTwo = pickTwo("일", "이", "삼");
System.out.println("pickTwo = " + Arrays.toString(pickTwo));
});
}
- 위의 클라이언트 코드를 이용하여 실행했을 때,
pickTwo()
의 결과는Object[]
타입으로 반환된다. 그러나,String[]
타입으로 받기 때문에 자바의 묵시적 캐스팅 때문에ClassCastException
이 날 것이다. - 힙 오염을 발생시킨 진짜 원인인
toArray()
와 매우 떨어져있어서 진짜 원인을 찾는데도 오래걸린다.
안전한 제네릭 가변인수 활용의 예
@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;
}
- 위의 메서드는 위험한 행위 2개를 하지 않았기에 안전하다.
- 제네릭 가변인수를 외부로 노출시키지 않았다.
- 제네릭 가변인수를 다른 배열에 담아서 조작하지 않았다.
- 또한
@SafeVarargs
애너테이션을 이용하여 안전함을 표시했고, 그래서 컴파일 에러도 뜨지 않는다.
단,
@SafeVarargs
는 재정의할 수 없는 메서드에만 달아야 한다. 상속할 때도 애노테이션이 이어지기 때문에 하위 타입의 구현이 정말로 안전한 가변인수인지는 알기 힘들다.
varargs 매개변수를 List로 대체할 수 있다.
static <T> List<T> flatten(List<List <? extends T>> lists) {
List<T> result = new ArrayList<>();
for(List<? extends T> list : lists) {
result.addAll(list);
}
return result;
}
- 이는 위의
flatten()
과 동일한 기능을 한다. @SafeVarargs
애너테이션과varargs
를 쓰는 것만이 항상 정답이 아님을 염두에 두자.
이전 pickTwo()
의 문제도 해결해보기
@Test
public void test2() {
List<String> pickTwo = pickTwo2("일", "이", "삼");
System.out.println("pickTwo = " + pickTwo);
}
public static <T> List<T> pickTwo2(T a, T b, T c) {
switch(ThreadLocalRandom.current().nextInt(3)) {
case 0: return List.of(a, b);
case 1: return List.of(a, c);
case 2: return List.of(b, c);
}
throw new AssertionError();
}
- 가변인자 메서드를 굳이 만들어 사용하지 않더라도,
List.of()
메서드를 통해 만들어진 리스트를 통해 마치 가변인자 메서드처럼 사용할 수 있다.List.of
메서드는 내부적으로@SafeVarargs
애너테이션이 붙어있다.
- 상대적으로 훨씬 안전하며, 자바 표준 API를 사용하여 더욱 편리하다.
핵심 정리
- 가변인수와 제네릭은 궁합이 썩 좋지 않다.
- 가변인수 기능은 배열을 노출하여 추상화가 완벽하지 못하고, 배열과 제네릭 타입의 규칙이 서로 다르다.
- 제네릭
varargs
매개변수는 타입 안전하지는 않지만, 허용된다.- 매우 편리하지만, 외부에
varargs
배열을 노출하거나, 내부적으로 다른 배열에 저장해놓고 변조하는 등의 일을 자제해야 한다. - 안전이 보장되었다면
@SafeVarargs
애너테이션을 이용하여 안전함을 표시하자.
- 매우 편리하지만, 외부에
반응형
'Java > 이펙티브 자바' 카테고리의 다른 글
이펙티브 자바, 쉽게 정리하기 - item 34. int 상수 대신 열거 타입을 사용하라 (0) | 2022.02.24 |
---|---|
이펙티브 자바, 쉽게 정리하기 - item 33. 타입 안전 이종 컨테이너를 고려하라 (0) | 2022.01.24 |
이펙티브 자바, 쉽게 정리하기 - item 31. 한정적 와일드카드를 사용해 API 유연성을 높이라 (0) | 2022.01.24 |
이펙티브 자바, 쉽게 정리하기 - item 30. 이왕이면 제네릭 메서드로 만들라 (0) | 2022.01.09 |
이펙티브 자바, 쉽게 정리하기 - item 29. 이왕이면 제네릭 타입으로 만들라 (0) | 2022.01.09 |