Post

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

가변인수와 제네릭

가변인수 메서드제네릭은 자바 5 때 함께 추가되었지만 잘 어우러지지 않는다. 🙅

가변인수 메서드 구현방식의 허점 🤔

  • 가변 인수 메서드를 호출하면 가변인수를 담기 위한 배열이 자동으로 하나 만들어진다.
  • 이 배열을 내부로 감췄어야 했는데, 클라이언트에 노출해버려서 제네릭이나 매개변수화 타입이 포함되면 컴파일 경고가 발생한다.

실체화 불가 타입은 런타임에는 컴파일타임보다 타입관련 정보를 적게 담고 있다. (아이템 28)

그리고 거의 모든 제네릭과 매개변수화 타입은 실체화되지 않는다.

제네릭과 가변인수(varargs)를 혼용하면 타입 안정성이 깨진다.

1
2
3
4
5
6
public void print(List<String> ... stringList) {
    List<Integer> intList = List.of(42);
    Object[] objects = stringList;
    objects[0] = intList; // 힙오염 발생 (매개변수화 타입 변수가 타입이 다른 객체 참조해서)
    String s = stringList[0].get(0); // ClassCastException
}

제네릭 배열을 프로그래머가 직접 생성하는 건 허용하지 않으면서, 제네릭 varargs 매개변수를 받는 메서드를 선언할 수 있게 한 이유는 무엇일까? (위 코드는 오류 대신 경고로 끝난다.)

  • 제네릭이나 매개변수화 타입의 varargs 매개변수를 받는 메서드가 실무에서 매우 유용하므로.
  • ex) Arrays.asList(T… a), Collections.addAll(Collection<? super T> c)

@SafeVarargs 애너테이션

  • 자바 7부터 @SafeVarargs 애너테이션이 추가되었다.
  • 제네릭 가변인수 메서드 작성자가 클라이언트 측에서 발생하는 경고를 숨길 수 있게 되었다.
  • @SafeVarargs 애너테이션은 메서드 작성자가 그 메서드가 타입 안전함을 보장하는 장치다.

제네릭 varargs 메서드가 타입 안전하려면..?

  • varargs 매개변수 배열에 아무것도 저장하지 않는다.
  • 그 배열을 신뢰할 수 없는 코드에 노출하지 않는다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 제네릭 varargs 매개변수 배열에 다른 메서드가 접근하도록 허용하면 안전하지 않다..
public class PickTwo {
    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(); // Can't get here.
    }
	
	public static void main(String[] args) {
        // Object[]는 String[]의 하위타입이 아니므로  ClassCastException 발생
        String[] attributes = pickTwo("Good", "Fast", "Cheap");
        System.out.prinln(Arrays.toString(attributes));
    }
}

제네릭 varargs 매개변수를 안전하게 사용하는 법

  • 제네릭이나 매개변수화 타입의 varargs 매개변수를 받는 메서드에 @SafeVarargs를 달아라.
    • 즉, 안전한 메서드만 작성해라.
  • 아이템 28의 조언을 따라 varargs 매개변수를 List 매개변수로 바꿀 수 도 있다.
1
2
3
4
5
6
7
8
9
@SafeVarargs
static <T> List<T> flatten(List<? extends T>... lists) {
    // ...
}

// 위 메서드를 List 매개변수로 대체
static <T> List<T> flatten(List<List<? extends T>> lists) {
    // ...
}

위의 pickTwo를 List 버전으로 바꾸면?

제네릭은 배열과 다르게 런타임 시점에 타입 정보를 소거하므로 캐스팅 에러가 나지 않는다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static <T> List<T> pickTwo(T a, T b, T c) {
    System.out.println(a.getClass().getName());
    switch (ThreadLocalRandom.current().nextInt(3)) {
        case 0:
            return Arrays.asList(a, b);
        case 1:
            return Arrays.asList(b, c);
        case 2:
            return Arrays.asList(a, c);
    }
    throw new AssertionError();
}

public static void main(String[] args) {
    String[] attributes = pickTwo("Good", "Fast", "Cheap");
}

💡 핵심 정리

  • 가변인수와 제네릭은 궁합이 좋지 않다. 🙅
    • 가변인수 기능은 배열을 노출하여 추상화가 완벽하지 못하다.
    • 배열과 제네릭의 타입 규칙이 서로 다르다.
  • 제네릭 varargs 매개변수는 타입 안전하진 않지만, 허용된다. (유용하기 때문)
  • 제네릭 varargs 매개변수를 사용하고 싶으면, 그 메서드가 타입 안전한지 확인한 후 @SafeVarargs를 달자.

참고 🕶️

쓰레드 지역 변수

  • https://docs.oracle.com/javase/8/docs/api/java/lang/ThreadLocal.html
  • 모든 멤버 변수는 기본적으로 여러 쓰레드에서 공유해서 쓰일 수 있다.
  • 이때 쓰레드 안전성과 관련된 여러 문제가 발생할 수 있다.
    • 경합 또는 경쟁조건 (Race-Condition)
    • 교착상태 (deadlock)
    • Livelock
  • 동기화를 하지 않아도 한 쓰레드에서만 접근 가능한 값이기 때문에 안전하게 사용할 수 있다.
  • 한 쓰레드 내에서 공유하는 데이터로, 메서드 매개변수에 매번 전달하지 않고 전역 변수처럼 사용할 수 있다.

스레드 지역 랜덤값 생성기

This post is licensed under CC BY 4.0 by the author.