제네릭
[아이템 32. 제네릭과 가변인수를 함께 쓸 때는 신중하라]
[가변인수]
0. 가변인수
public void method(String... str){
}
...
을 사용하여 메서드의 parameter를 구성하는 코드가 가변인수를 정의하는 코드이다.
1. 가변인수의 등장
- 가변인수(varargs)는 제네릭과 함께 자바 5에서 등장한 개념이다.
2. 가변인수란?
- 메서드에 넘기는 인수의 개수를 클라이언트가 조절할수 있게 하는 인수이다.
3. 가변인수 메서드의 호출
- 가변인수 메서드를 호출하면 가변인수를 담기 위한 배열이 자동으로 하나 만들어진다.
4. 가변인수의 허점
- 내부로 해당 정보를 감춰야 하는데 가변인수를 사용할 경우엔 가변인수를 담기 위한 배열을 클라이언트에게 그대로 노출한다는 문제점이 발생한다.
[가변인수로 실체화 불가 타입을 사용할 때]
1. 실체화 불가 타입
E
,List<E>
,List<String>
등을 실체화 불가 타입이라고 하는데 해당 타입을 가변 인수로 넘겨주면 컴파일러가 경고를 날린다.
- 실체화 불가 타입은 런타임시 컴파일타임보다 타입 관련 정보를 적게 담고 있다. 제네릭과 같은 실체화 불가 타입은 컴파일 시 타입을 벗겨내기 때문이다.
- 또한 제네릭과 매개변수화 타입은 대게 대부분이 실체화되지 않는다.
2. 매개변수화 타입 변수가 다른 객체를 참조할 경우엔 힙 오염이 발생한다.
static void dangerous(List<String>... stringLists){
List<Integer> intList = List.of(42);
Object[] objects = stringLists;
obcts[0] = intList;
String s = stringLists[0].get(0);
- 매개변수화 타입의 변수가 타입이 다른 객체를 참조하면 힙 오염이 발생한다.
- 이는 제네릭으로 약속한 타입 시스템 안전성의 근간을 흔들어버리는 일이다.
[실무에선 유용한 가변인수로 실체화 불가 타입을 사용하는 것]
1. 경고로 끝나는 오류
- 위의 가변인수로 매개변수화 타입을 넣는 행위가 경고로만 끝나고 있다. 왜일까?
- 이는 제네릭이나 매개변수화 타입의 가변인수 매개변수를 받는 메서드 가실무에서는 매우 유용하기 때문이다.
- 그렇기 때문에 설계자가 이 모순을 받아들이기로 했다.
2. 모순을 받아들여 적용된 라이브러리들
Arrays.asList(T... a)
Collections.addAll(Collection<? super T> c, T... elements)
EnumSet.of(E first, E... rest)
- 위의 보여준 예시와 달리 해당 라이브러리들의 타입은 안전하다.
[@SafeVarargs]
1. 자바 7전의 상황
- 자바 7 전에는 제네릭 가변인수 메서드의 작성자가 호출자 쪽에서 발생하는 경고에 대해서 해줄 수 있는 일이 없었다.
- 그래서 단순히 @SuppressWarnings("unchecked") 애너테이션을 달아서 경고를 숨겨야 했다.
- 때로는 이 작업이 진짜 문제를 알려주는 경고를 숨겨 안 좋은 결과로 이어졌다.
2. 경고를 숨기는 @SafeVarargs
- 자바 7부터는 해당 애너테이션이 추가 되어, 클라이언트 측에서 발생하는 경고를 숨길 수 있게 됐다.
- 해당 애너테이션은 메서드 작성자가 그 메서드가 타입 안전성을 보장하는 장치로 사용한다.
- 주의점은 해당 메서드가 안전하지 않다면 @SafeVarargs 애너테이션을 달지 않아야 한다.
3. 가변인수에 매개변수화 타입을 사용해도 타입 안전성을 보장할 수 있는 경우
- 가변인수 메서드를 호출하면 가변인수를 담을 배열이 생성됨을 일단 기억하자.
- 메서드가 이 배열에 아무것도 저장하지 않고, 그 배열의 참조가 밖으로 노출되지 않는다면 타입이 안전하다고 할수 있다.
- 매개변수 배열이 호출자로부터 그 메서드로 순수하게 인수들을 전달하는 일만 한다면 그 메서드는 안전하다.
[가변인수 사용에도 메서드가 안전함을 확인하는 방법]
1. 안전하지 않은 경우
1-1. 자신의 제네릭 매개변수 배열의 참조를 노출하는 경우
static <T> T[] toArray(T... args){
return args;
}
- 배열의 타입은 컴파일 타임에 결정되는데, 그 시점에 잘못 판단할 수 있다.
- varargs 매개변수 배열을 그대로 반환할 경우 힙 오염을 이 메서드를 호출한 쪽의 콜스택으로까지 전이하는 결과가 생길 수 있다.
1-2. 매개변수 배열에 다른 변수가 접근하는 경우
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);
}
}
public static void main(String[] args){
String[] attributes = pickTwo("좋은", "빠른", "저렴한");
}
- 위의 메서드는 제네릭 가변인수를 받는 toArray()를 호출하고 있다.
- 이 코드가 만들 배열의 타입은 Object[]이고 main 메서드에서 사용할 땐 String[]으로 형변환해서 받아온다.
- 이때 String[]으로 자동 형변환 하는 코드가 컴파일러에서 자동 생성하고 이는 Object[]가 String[]의 하위 타입이 아니여서 문제가 생기게 된다.
2. 안전하게 사용한 경우
@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;
}
- 임의의 개수의 리스트를 인수로 받아 받은 순서대로 그 안의 원소를 하나의 리스트로 옮겨 담아서 반환해야 한다.
- @SafeVarargs 애너테이션을 달아주었기 때문에 선언하는 쪽과 사용하는 쪽 모두에서 경고를 내지 않는다.
[@SafeVarargs와 관련된 여러 경우]
1. @SafeVarargs를 사용하지 않아야 할 두가지 경우
- varargs 매개변수 배열에 아무것도 저장하지 않는다.
- 그 배열(혹은 복제본)을 신뢰할 수 없는 코드에 노출하지 않는다.
- 재정의할 수 있는 메서드에는 해당 애너테이션을 달수 없다. (재정의 메서드 역시 안전을 보장할 수 없기 때문이다.)
2. @SafeVarargs를 대체하는 방법
static <T> List<T> flatten(List<List<? extends ?>> lists){
List<T> result = new ArrayList<>();
for (List<? extends T> list: lists)
result.addAll(list);
return result;
}
- 속도가 살짝 느려지고, 코드가 지저분해진다는 단점이 있지만 실수로 안전하다고 판단할 일이 없어진다.