[item#32] 제네릭과 가변인수를 함께 쓸 때는 신중하라
제네릭과 가변인수를 함께 쓸 때는 신중하라
가변인수(varargs) 메서드와 제네릭은 자바 5에서 같이 추가되었으나 잘 어울리지 못한다.
- 가변인수의 허점
가변인수 메서드를 호출하면 가변인수를 담기 위한 배열이 만들어짐.
=> 내부로 감춰야 했을 이 배열을 클라이언트에 노출하는 문제가 생김.
=> varargs 매개변수에 제니릭이나 매개변수화 타입이 포함되면 알기 어려운 컴파일 경고각 발생
메서드를 선언할 때 실체화 불가 타입(예로 제네릭과 매개변수화 타입)으로 varargs 매개변수를 선언하면 컴파일러가 경고를 보냄.
가변인수 메서드를 호출할 때도 varargs 매개변수가 실체화 불가 타입으로 추론되면 그 호출에 대해서도 경고를 냄.
warnig: [unchecked] Possible heep pollution from
parameterized vararg type List<String>
매개변수화 타입의 변수가 타입이 다른 객체를 참조하면 힙 오염이 발생한다.
이렇게 다른 타입 객체를 참조하는 상황에서는 컴파일러가 자동 생성한 형변환이 실패할 수 있으니,
제네릭 타입 시스템이 약속한 타입안전성의 근간이 흔들려버린다.
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
}
위처럼 타입 안전성이 깨지니 제네릭 varargs 배열 매개변수에 값을 저장하는 것은 안전하지 않다.
제네릭 배열은 안되지만 제네릭 varargs 는 되는 이유?
Arrays.asList(T... a)
Collections.addAll(Collection<? super T> c, T... elements)
EnumSet.of(E first, E... rest)
간단하게 실무에서 유용하기 때문.
위의 예시는 그 전의 예시와는 다르게 타입 안전하다.
자바 7 전에는 제네릭 가변인수 메서드의 작성자가 호출자 쪽에서 발생하는 경고에 대해 해줄 수 있는 일이 없었다.
=> 이런 메서드는 사용하기에 꺼림칙 했음.
=> 사용자들은 경고를 그냥 두거나 숨겨야 했음(@SuppressWarnings)
=> 때로는 진짜 문제를 알려주는 경고도 숨기는 결과가 생김.
자바 7에서는 @SafeVarars 애너테이션을 추가해 제네릭 가변인수 메서드 작성자가 클라이언트 측에서 발생하는 경고를 숨길 수 있게됨.
즉, @SafeVarargs 는 메서드 작성자가 메서드가 타입 안전함을 보장하는 장치임.
@SafeVarargs 는 컴파일러가 메서드가 안전하지 않을 수 있다는 경고를 하기 않게 만들어주기 때문에 확실하지 않다면 달면 안됨.
- 어떻게 안전한지 확신?
가변인수 메서드를 호출할 때 varargs 매개변수를 담는 제네릭 배열이 만들어짐.
=> 메서드가 이 배열아 아무것도 저장하지 않고 배열의 참조가 밖으로 노출되지 않으면
=> 타입 안전.
(순수하게 인수를 전달하는 일만 한다면 안전함)varargs 매개변수 배열에 아무것도 저장하지 않고도 타입 안전성을 깰 수도 있다?
static <T> T[] toArray(T... args){ return args; }
위 메서드가 반환하는 배열의 타입은 메서드에 인수를 넘기는 컴파일 타임에 결정되는데
그 시점에는 컴파일러에게 충분한 정보가 주어지지않아 타입을 잘못 판단 할 수 있다.
따라서 varargs 매개변수 배열을 그대로 반환하면 힙 오염을 메서드를 호출한 쪽의 콜스택으로 전이할 수도 있음.
제네릭 varargs 매개변수 배열에 다른 메서드가 접근하도록 허용하면 안전하지 않다.
- 예외상황
- @SafeVarargs로 애노테이트된 다른 varargs 매서드에 넘기는 것은 안전
- 배열 내용의 일부 함수를 호출만 하는(varargs를 받지 않는) 일반 메서드에 넘기는 것도 안전
- @SafeVarargs로 애노테이트된 다른 varargs 매서드에 넘기는 것은 안전
// 제네릭 varargs를 안전하게 사용하는 예
@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 를 사용할 때를 정하는 규칙
=> 제네릭이나 매개변수화 타입의 vvarargs 매개변수를 받는 모든 메서드에 @SafeVarargs 를 달라.
그래서 사용자를 헷갈리게 하는 컴파일러 경고를 없앨 수 있다.
- 둘중 하나라도 어기면 수정!
- varargs 매개변수 배열에 아무것도 저장하지 않는다.
- 그 배열(혹은 복제본)을 신뢰할 수 없는 코드에 노출하지 않는다.
- varargs 매개변수 배열에 아무것도 저장하지 않는다.
@SafeVarargs 는 재정의할 수 없는 메서드에만 달아야 함.
재정의한 메서드도 안전할지 보장이 안되기 때문.
자바 8에서는 오직 정적 메서드와 final 인스턴스 메서드에만 붙일 수 있었고
자바 9부터는 private 인스턴스 메서드에도 허용됨.
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);
}
reutnr result;
}
// 정적 팩터리 메서드인 List.of 를 사용하면 임의의 개수의 인수를 넘길 수 있음.
// List.of 에도 @SafeVarargs 가 달려있기 때문에 가능
audience = flatten(List.of(friends, romans, countrymen));
- 장점
이 방식은 컴파일러가 이 메서드의 타입 안전성을 검증할 수 있음.
@SafeVarargs 애너테이션을 달지 않아도 됨.
실수로 안전하다고 판단할 걱정 없음. - 단점
클라이언트 코드가 지저분해짐
속도가 약간 느려질 수 있음.
ThreadLocal
멀티 스레드 환경에서는 신경써야할 것들이 아주 많음.
스레드에 안전하게 코드를 짜는것, 교착 상태, lock lock 등등
이때 ThreadLocal을 사용하면 스레드 전용 지역변수를 만들어서 해결할 수 있다.
ThreadLocalRandom
Random 의 next를 쓸때는 결과적으로 하나의 next를 사용하는데 AtomicLong이라는 클래스를 사용한다.
AtomicLong 은 멀티 스레드 환경에서 lock을 사용하지 않고 안전하게 사용할 수 있는 클래스임.
lock을 기다리고 주고 받는 시간때문에 lock은 성능에 영향을 미친다.
- 염세적인 lock : lock을 기다리고 주고받음
- 낙관적인 lock : 일단 메서드 안으로 들어가고 사용중이면 실패하거난 재시도함.
Random 안의 compareAndSet이 이런 방식으로 작동 되다보니
여러 스레드에서 next를 많이 호출하면 어느하나는 꼭 실패 및 재시도가 발생한다.
=> 성능에 문제가 생김
이럴거면 ThreadLocalRandom 을 쓰자.