-
Notifications
You must be signed in to change notification settings - Fork 2
Description
Chapter : 5. 제네릭
Item : 32. 제네릭과 가변인수를 함께 쓸 때는 신중하라
Assignee : eunpyeong114
🍑 서론
가변 인수(Varargs)란 무엇인가?
Varargs는 variable arguments의 준말로, 자바에서 메소드의 인자는 임의의 value 개수를 받을 수 있다.
이렇게 변수 임의의 개수를 받는 인자를 varargs라고 한다.
사용하는 이유?
자바 메소드를 하나 만든다고 가정할 때, 우리는 이 메소드가 받아들이고자 하는 인자가 얼마나 많을지 장담할 수 없기 때문이다.
사용법
… (점 세 개) 가 메소드의 파라미터로 사용된다.
class Math {
int sum = 0;
public int plus(int... nums) {
int sum = 0;
for(int x : nums) {
sum += nums;
}
return sum;
}
}Varargs 작동 원리
…구문은 자바 컴파일러에게 이 메소드는 0개 이상의 인자로 호출될 수 있다고 알려준다.
결과적으로, 위의 nums 변수는 int[] 타입의 배열로 암묵적으로 선언된다.
따라서, 메소드 안에서 num 변수는 array 구문을 사용해 접근한다. 만약 어떤 인자도 없다면, nums의 길이는 0이 된다.
🍑 본론
문제점
클라이언트에서 가변 인수 메서드를 호출하면 가변 인수를 담기 위한 배열이 자동으로 만들어지는데, 이 배열을 내부로 감추지 못하고 클라이언트에 노출하는 문제가 생김.
그 결과 varargs 매개변수에 제네릭이나 매개 변수화 타입이 포함되면 알기 어려운 컴파일 경고가 발생함.
아이템28)
: 실체화 불가 타입은 런타임에는 컴파일타임보다 타입 관련 정보를 적게 담고 있음
: 거의 모든 제네릭과 매개변수화 타입은 실체화되지 않는다.
가변인수 메서드를 호출할 때도 varargs 매개변수가 실체화 불가 타입으로 추론되면, 그 호출에 대해서도 호출을 보낸다.
warning: [unchecked] Possible heap pollution from
parameterized vararg type List<String>
매개변수화 타입의 변수가 타입이 다른 객체를 참조하면 힙 오염이 발생한다.
힙 오염?
특정 타입의 참조로 예상되는 컬렉션이나 배열 등에 잘못된 타입의 객체가 저장되어 프로그램의 다른 부분에서 타입 캐스팅 오류가 발생하는 상황
이렇게 다른 타입 객체를 참조하는 상황에서는 컴파일러가 자동 생성한 형변환이 실패할 수 있으니, 제네릭 타입 시스템이 약속한 타입 안전성의 근간이 흔들려버린다.
제네릭과 varargs를 혼용하면 타입 안전성이 깨진다
static void dangerous(List<String>... stringLists) {
List<Integer> intList = List.of(42);
Object[] objects = stringLists;
Object[0] = intList; // 힙 오염 발생
String s = stringLists[0].get(0); // ClassCastException
}이렇게 타입안정성이 깨지니 제네릭 varargs 배열 매개변수에 값을 저장하는 것은 안전하지 않다.
@SafeVarargs
: 메서드 작성자가 그 메서드가 타입 안전함을 보장하는 장치
: 메서드가 안전한 게 확실하지 않다면 절대 사용해선 안 됨
안전한 제네릭 varargs 메서드는?
아래 두가지 조건을 충족하면 안전하다!
- varargs 매개변수 배열에 아무것도 저장하지 않는다.
- 그 배열(혹은 복제본)을 신뢰할 수 없는 코드에 노출하지 않는다.
추가적으로
@SafeVarargs는 재정의할 수 없는 매서드에만 달아야 한다. 재정의한 메서드도 안전할지는 보장할 수 없기 때문이다.
자바8에서는 오직 정적 메서드와 final 인스턴스 메서드에만 사용가능
자바9부터는 추가적으로 private 인스턴스 메서드에도 허용
@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;
}정적 팩터리 메서드인 List.of를 활용하면 이 메서드에 임의 개수의 인수를 넘길 수 있다. 이는 List.of에도 @SafeVarargs가 달려있기 때문!
audience = flatten(List.of(friends, romans, countrymen));List.of 메소드의 주요 특징
- 불변성 : 생성된 리스트에 대한 어떠한 변형도 허용하지 않음.
- null 요소 불허 : List.of에 전달된 요소 중 하나라도 null이면 NullPointerException을 발생. 이는 리스트가 항상 null이 아닌 요소만 포함하도록 보장함.
- 직관적인 사용법 : 정적 메소드를 사용하기 때문에, 새로운 리스트를 생성하고 초기화하는 코드를 단순화
- 스레드 안전성 : 생성된 리스트는 변경할 수 없으므로 여러 스레드에서 동시에 접근해도 문제가 발생하지 않음
이 방식은 컴파일러가 메서드 타입 안정성을 검증할 수 있다는 장점이 있지만, 코드가 살짝 지저분해지며 속도가 조금 느려질 수 있다는 사소한(?) 단점이 존재함
🍑 결론
가변인수와 제네릭은 궁합이 좋지 않다.
제네릭 varargs 매개변수는 타입 안전하지는 않지만, 허용된다.
메서드에 제네릭 varargs 매개변수를 사용하고자 한다면, 먼저 그 메서드가 타입 안전한지 확인한 다음 @SafeVarargs 어노테이션을 달아 사용하는데 불편함이 없게끔 하자!