제네릭과 가변인수를 함께 쓸때는 신중하라

수박참외메론·2023년 1월 15일
0

가변인수 메서드

아이템 53에 나왔는데 왜 제네릭이랑 같이 묶여서 아이템 32에 나오는가...
아마도 Java 5 에 같이 추가가 되어 그런듯 하다.

가변인자

  • 가변인자는 가변인자를 나타내는 ...를 타입 뒤에 붙여 해당 인자가 가변 인자임을 나타내서 정의한다.
  • 가변인자는 컴파일 시 배열로 처리되기 때문에 사용할 때 해당사항을 주의해야 한다.
  • 가변인자는 매개변수 중에서 제일 마지막에 선언해야 된다. (안그러면 컴파일오류)
@SafeVarargs
@SuppressWarnings("varargs")
static <E> List<E> of(E... elements) {
    switch (elements.length) { // implicit null check of elements
        case 0:
            return ImmutableCollections.emptyList();
        case 1:
            return new ImmutableCollections.List12<>(elements[0]);
        case 2:
            return new ImmutableCollections.List12<>(elements[0], elements[1]);
        default:
            return new ImmutableCollections.ListN<>(elements);
        }
    }

가변인수 메서드를 호출하면 가변인수를 담기 위한 배열이 자동으로 하나 만들어진다.

이 가변인수 메서드를 호출할 때도 varargs 매개변수가 실체화 불가 타입으로 추론되면, 그 호출에 대해서도 경고를 낸다.

warning : [unchecked] Possible heap pollution from 
	parameterized vararg type List<String> 

힙 오염이란?

  • JVM 의 메모리 공간인 Heap Area 가 오염된 상태를 Heap pollution 이라고 한다.
  • 주로 매개변수화 타입의 변수가 타입이 다른 객체를 참조할 때 발생한다.

제네릭과 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
}

위 코드에서 힙 오염이 발생한 이유는

  • 가변인자 List<String> 배열을 받아서 objects 배열을 초기화 시켜주고
  • 인덱스 0의 요소를 List<Integer> 타입의 변수로 초기화 시켜줄때
  • objects 배열에 다른 타입 두가지가 존재하기 때문에 힙 오염이 발생한다.
  • 이 메서드에서는 형변환하는 곳이 보이지 않지만, 마지막 줄에 컴파일러가 형변환을 시켜주기 때문에 ClassCastException 이 발생하게 된다.
  • 이처럼 타입 안정성이 깨지니 제네릭 varargs 배열 매개변수에 값을 저장하는 것은 안전하지 않다.
  • 컴파일러는 저런 제네릭 varargs 매개변수를 받는 메서드에서는 위에서 봤던 경고를 띄워주게 된다.
  • 이 경고는 어떻게 없애주면 될까? 이전에 하던대로 @SuppressWarnings("unchecked") 으로 하면 될까?

제네릭 배열은 안됐으면서 제네릭 varargs 배열은 왜 되는가?

  • 제네릭 배열은 자바에서 금지됐지만, 제네릭 varargs 는 내부적으로 제네릭 배열을 생성함에도 금지되지 않았다.
  • 왜 제네릭 배열은 컴파일러 단에서 오류를 발생시키는데, 제네릭 매개변수는 경고만 호출되고 가능하게 냅뒀을까?
  • 실제로 generic varargs는 매우 유용하게 사용할 수 있기 때문이다.
    - Array.asList(T... a)
    • Collections.addAll(Collection<? super T> c, T... elements)
    • EnumSet.of(E first, E... rest)
    • 힙 오염의 위험이 있을 수 있기 때문에 이 함수들은 타입 안전하다는 것을 보장하면서 제공된다.

@SafeVarargs

  • 자바 7부터는 @SafeVarargs 애너테이션이 추가되어 제네릭 가변인수 메서드 작성자가 클라이언트 측에서 발생하는 경고를 숨길 수 있게 되었다.
  • @SafeVarargs annotation은 메서드 작성자가 그 메서드가 타입 안전함을 보장하는 장치이다.
  • 컴파일러는 해당 annotation 을 믿고 메서드가 안전하지 않다는 경고를 없애기 때문에, 메서드가 안전함이 확실할때만 해당 annotation 을 사용해줘야 한다.
  • 메서드가 안전하다는 것을 확신할 수 있을 때
    1. 메서드가 배열에 아무것도 저장하지 않고
    1. 그 배열의 참조가 밖으로 노출되지 않는다면

varargs 매개변수 배열에 다른 메서드가 접근하도록 허용하면 안전하지 않다.

static <T> T[] toArray(T... args) {
	return args;
}
  • 자신의 제네릭 매개변수 배열의 참조를 노출하는 케이스이다.
  • 이 메서드가 반환하는 배열의 타입은 이 메서드에 인수를 넘기는 컴파일 타임에 결정된다.
  • 그 시점에는 컴파일러에게 충분한 정보가 주어지지 않아 타입을 잘못 판단할 수 있다.
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 메서드를 호출한다는 점만 빼면 안전하다.
  • 이 메서드를 본 컴파일러는 toArray 에 넘길 T 인스턴스 2개를 담을 varargs 매개변수 배열을 만드는 코드를 생성한다.
  • 이 코드가 만드는 배열 타입은 pickTwo 에 어떤 타입의 객체를 넘기더라도 담을 수 있는 가장 구체적인 타입인 object[] 이다.
public static void main (String[] args) {
	String[] attributes = pickTwo("좋은", "빠른", "저렴한");
}
  • 위의 예시에서 아무 문제가 없는 메서드이니 별다른 경고 없이 컴파일 된다.
  • 하지만 실행시키려 하면 ClassCastException 을 던진다.
  • pickTwo 함수는 무조건 Object[] 을 반환하지만 main 함수에서는 String[] 으로 받기 때문이다.

그래서 결과적으로 제네릭 varargs 매개변수 배열에 다른 메서드가 접근하도록 허용하면 안전하지 않다는 것이다.

안전하게 사용하는 법

  • 안전함을 보장하는 메서드를 만들어 @SafeVarargs 를 사용해 warning 제거
@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;
}

위에서 말했듯이 아래의 두가지 법칙을 지키면 안전하다.
1. varargs 매개변수 배열에 아무것도 저장하지 않는다.
2. 그 배열(혹은 복사본)을 신뢰할 수 없는 코드에 노출하지 않는다.

  • 제너릭 타입 배열을 생성하지 않도록 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;
}

아이템 28에 따라 배열말고 리스트를 사용하여 안전하게 바꾼 모습이다.

이와 같은 방식으로 앞서 설명했던 toArray를 대체하여 안전하게 pickTwo 메서드를 재정의할 수 있다.

static <T> List<T> pickTwo(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();
}

그리고 클라이언트 코드는 아래와 같이 바뀐다.

public static void main (String[] args) {
	List<String> attributes = pickTwo("좋은", "빠른", "저렴한");
}
profile
하루하루는 성실하게 인생전체는 되는대로

0개의 댓글