Home [Effective Java] Item32. 제네릭과 가변인수를 함께 쓸 때는 신중하라!
Post
Cancel

[Effective Java] Item32. 제네릭과 가변인수를 함께 쓸 때는 신중하라!

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

  • 가변인수 메서드와 제네릭은 잘 어우러지지 않는다.

가변 인수의 문제점

  • 가변인수는 메서드에 넘기는 인수의 개수를 클라이언트가 조절할 수 있다. 구현 방식에 허점이 있다.
    • 메서드 호출 시 가변인수를 담기 위한 배열 생성
      • 이 배열을 클라이언트에 노출시키는 문제가 생김.
  • 위와 같은 문제로 인해 가변인수(varargs) 매개변수에 제네릭이나 매개변수화 타입이 포함되면 알기 어려운 컴파일 경고가 발생한다.

실체화 불가 타입

  • 런타임에서 컴파일타임보다 타입 관련 정보를 적게 담고 있는다.
  • 거의 모든 제네릭과 매개변수화 타입은 실체화되지 않는다.
  • 메서드 선언 시 실체화 불가 타입으로 가변인수 매개변수를 선언하면 컴파일러가 경고를 보낸다.
    • 가변인수 메서드도 마찬가지로 varargs 매개변수가 실체화 불가 타입이라 추론될 경우, 경고를 준다.
      • 그 경고는 힙 오염 발생이다.
        • 다른 타입 객체 참조 시, 컴파일러가 자동 생성한 형변환의 실패 가능성이 있어 제네릭이 약속한 타입 안전성의 근간이 흔들린다.

제네릭과 가변인수 혼용

1
2
3
4
5
6
static void dangerous(List<String>... stringLists) {
		List<Integer> intList = List.of(42);
		Object[] objs = stringLists;
		objs[0] = intList;									// 힙 오염 발생
		String s = stringLists[0].get(0);		// ClassCastException
}
  • 마지막 줄에 컴파일러가 생성한 (보이지 않는) 형변환이 숨어있다.
    • 이 경우 형변환에 실패해 에러를 던진다.
  • 타입 안전성을 위해 제네릭 varargs 배열 매개변수에 값을 저장하는 것은 안전하지 않다.

제네릭 배열 생성을 허용하지 않는데, 제네릭 varargs 매개변수 받는 메서드 선언을 허용하는 이유는?

  • 제네릭이나 매개변수화 타입의 varargs 매개변수를 받는 메서드가 실무에서 매우 유용히 사용된다.
    • 그래서 언어 설계자는 이 모순을 수용했다.
    • Arrays.asList(T… a), Collections.addAll(Collection<? super T> c, T…elements), EnumSet.of(E first, E… rest)와 같은 대표적인 예가 존재한다.
      • 위에 기입한 예는 위험한 메서드들과는 달리 타입 안전하다.

@SafeVarargs 애너테이션

  • 메서드 작성자가 그 메서드가 타입 안전함을 보장하는 장치다.
  • 제네릭 가변인수 메서드 작성자가 클라이언트 측에서 발생하는 경고를 숨길 수 있다.
  • 메서드 안전이 보장되지 않으면 절대 @SafeVarargs 애너테이션을 달아서는 안 된다.

메서드 안전을 확신할 수 있는 경우

  1. varargs 매개변수 배열에 아무것도 저장하지 않는 경우

  2. 배열의 참조가 밖으로 노출되지 않는 경우

  • 정리해보면, varargs 매개변수 배열이 (just) 인수전달 목적으로만 사용되면 메서드는 안전하다.

안전하게 제네릭 varargs 매개변수 사용하기

  1. @SafeVarargs로 제대로 애노테이트된 또 다른 varargs 메서드에 넘기는 것
  2. 그저 배열 내용의 일부 함수를 호출만 하는 일반 메서드에 넘기는 것
  • 안전한 사용의 예

    1
    2
    3
    4
    5
    6
    7
    
    @SafeVarargs
    static <T> List<T> saveCapsule(List<? extends T>... lists) {
        List<T> result = new ArrayList<>();
        for (List<? extends T> list : lists)
            result.addAll(list);
        return result;
    }
    
    • 임의 개수의 리스트를 인수로 받아, 받은 순서대로 그 안의 모든 원소를 옮겨 담아 반환한다.

@SafeVarargs 애너테이션 사용 규칙

  • 제네릭이나 매개변수화 타입의 varargs 매개변수를 받는 모든 메서드에 @SafeVarargs 애너테이션을 달자.
    • 이 말은 동시에 안전하지 않은 varargs 메서드는 절대 작성해서는 안된다는 뜻.

List.of를 이용하는 방법

  • varargs 매개변수를 List 매개변수로 바꾼다.
  • 정적 팩터리 메서드인 List.of를 활용해 임의 개수의 인수를 넘길 수 있다.
    • List.of 역시 @SafeVarargs 애너테이션이 달려있기 때문에 활용 가능하다.

List.of를 이용한 예제

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class SafePickTwo {
    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("좋은", "빠른", "저렴");
    }
}
  • 이 방식의 장점은 컴파일러가 메서드의 타입 안전성을 검증할 수 있다는 데에 있다.
  • 위 예제의 결과 코드는 배열 없이 제네릭만 사용하므로 타입 안전하다.

핵심 정리

  • 가변인수와 제네릭은 궁합이 좋지 않다.
    • 가변인수 기능은 배열을 노출한다. 고로, 추상화가 완벽하지 못하다.
    • 배열과 제네릭의 타입 규칙이 서로 다르다.
  • 제네릭 varargs 매개변수는 타입 안전하지는 않다. 그러나 허용된다.
  • 메서드에서 제네릭(또는 매개변수화된) varargs 매개변수 사용을 하려는 경우
    • 그 메서드가 타입 안전한지 확인한다.
    • @SafeVarargs 애너테이션을 달아 사용의 불편함을 제거한다.
This post is licensed under younghwani by the author.

[Effective Java] Item31. 한정적 와일드카드를 사용해 API 유연성을 높이라!

[Effective Java] Item33. 타입 안전 이종 컨테이너를 고려하라!

Comments powered by Disqus.