가변인수와 제네릭은 자바 5때 함께 추가되었다.
이 둘은 궁합이 좋지 않다.
가변인수 기능은 배열을 노출하여 추상화가 완벽하지 못하다. 그리고 배열과 제네릭은 타입 규칙이 서로 다르다.
이 두 부분에서 궁합이 맞지 않는다.
가변인수 동작 방식
가변인수 메서드를 호출하면 가변인수를 담기위한 배열이 생성된다.
그런데 내부로 감춰야 했을 이 배열을 클라이언트에 노출하는 문제가 있다.
제네릭과 같은 실체화 불가 타입 런타임에서 컴파일보다 타입 관련 정보를 적게 담고 있다.
따라서 메서드 선언 시 가변인수 매개변수로 제네릭을 선언한다면 컴파일러가 경고를 보낸다.
제네릭과 varargs를 혼용하면 타입 안정성이 깨진다!
static void dangerous(List<String>... stringLists) {
List<Integer> intList = List.of(42);
Object[] objects = stringLists;
objects[0] = intList; // 힙 오염 발생
String s = stringLists[0].get(0); // ClassCastException
}
위의 코드 마지막줄에서 예외가 발생한다. 이 메서드에서는 형변환하는 곳이 보이지 않는데도 호출하면 예외가 발생한다.
이처럼 타입 안전성이 깨지기 때문에 제네릭은 가변인수 매개변수로 안전하지 않다.
그렇다면 재네릭을 가변인수로 선언할 수 있게 한 이유는 뭘까?
정답 : 실무에서 매우 유용하기 때문이다. 그래서 설계자들이 이 모순을 수용하기로 했다.
@SafeVarags
@SafeVarags 애너테이션은 메서드 작성자가 그 메서드가 타입 안전함을 보장하는 장치다.
컴파일러는 이 애너테이션을 보고 경고를 더 이상 하지 않는다.
이 애너테이션은 메서드가 완전 안전할 때만 !!! 달자.
그리고 재정의 할 수 없는 메서드에만 달아야한다.
재정의한 메서드도 안전할지는 보장할 수 없기 때문이다.
그렇다면 안전하다고 확신하려면 어떻게 할까?
가변인수를 담기위한 배열이 생성될 때 메서드가 이 배열에 아무것도 저장하지 않고,
그 배열의 참조가 외부로 노출되지 않는다면 안전하다.
다시 말해서, 가변인수가 매개변수들을 순수하게 전달하기만 한다면 그 메서드는 안전하다.
ex) 참조를 외부로 노출하는 코드 - 안전하지 않음
static <T> T[] toArray(T... args) {
return args;
}
ex) 가변인수를 안전하게 사용하는 메서드
@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;
}
}
@SafeVarags 애너테이션를 쓰지 않고 안전하게 사용할 수 있는 방법
@SafeVarargs
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;
}
}
이렇게 사용하면 @SageVarags 애너테이션을 우리가 직접 달지 않아도 된다.
그리고 안전 여부를 판단하지 않아도 된다.
정리
가변인수와 제네릭은 궁합이 좋지 않으므로 사용을 자제하자.
사용하고자 한다면 안전한지 확인 후 @SafeVarargs 애너테이션을 달자
'우아한테크코스 > 공부' 카테고리의 다른 글
빈 스코프 - prototype (1) | 2022.05.26 |
---|---|
적시에 방어적 복사본을 만들라 (0) | 2022.04.04 |
태그 달린 클래스보다는 클래스 계층구조를 활용하라 (0) | 2022.03.29 |
finalizer와 cleaner 사용을 피하라 (0) | 2022.03.22 |
상속을 고려해 설계하고 문서화하라. 그러지 않았다면 상속을 금지하라 (0) | 2022.03.15 |
댓글