Skip to content

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

@pyeong114

Description

@pyeong114

Chapter : 5. 제네릭

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

Assignee : eunpyeong114


🍑 서론

가변 인수(Varargs)란 무엇인가?

Varargs는 variable arguments의 준말로, 자바에서 메소드의 인자는 임의의 value 개수를 받을 수 있다.

이렇게 변수 임의의 개수를 받는 인자를 varargs라고 한다.

사용하는 이유?

자바 메소드를 하나 만든다고 가정할 때, 우리는 이 메소드가 받아들이고자 하는 인자가 얼마나 많을지 장담할 수 없기 때문이다.

사용법

(점 세 개) 가 메소드의 파라미터로 사용된다.

class Math {
  int sum = 0;
  public int plus(int... nums) {
   int sum = 0; 
   for(int x : nums) {
      sum += nums;
    }
    return sum;
  }
}

Varargs 작동 원리

구문은 자바 컴파일러에게 이 메소드는 0개 이상의 인자로 호출될 수 있다고 알려준다.

결과적으로, 위의 nums 변수는 int[] 타입의 배열로 암묵적으로 선언된다.

따라서, 메소드 안에서 num 변수는 array 구문을 사용해 접근한다. 만약 어떤 인자도 없다면, nums의 길이는 0이 된다.

🍑 본론

문제점

클라이언트에서 가변 인수 메서드를 호출하면 가변 인수를 담기 위한 배열이 자동으로 만들어지는데, 이 배열을 내부로 감추지 못하고 클라이언트에 노출하는 문제가 생김.

그 결과 varargs 매개변수에 제네릭이나 매개 변수화 타입이 포함되면 알기 어려운 컴파일 경고가 발생함.

아이템28)
: 실체화 불가 타입은 런타임에는 컴파일타임보다 타입 관련 정보를 적게 담고 있음
: 거의 모든 제네릭과 매개변수화 타입은 실체화되지 않는다.

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

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

매개변수화 타입의 변수가 타입이 다른 객체를 참조하면 힙 오염이 발생한다.

힙 오염?
특정 타입의 참조로 예상되는 컬렉션이나 배열 등에 잘못된 타입의 객체가 저장되어 프로그램의 다른 부분에서 타입 캐스팅 오류가 발생하는 상황

이렇게 다른 타입 객체를 참조하는 상황에서는 컴파일러가 자동 생성한 형변환이 실패할 수 있으니, 제네릭 타입 시스템이 약속한 타입 안전성의 근간이 흔들려버린다.

제네릭과 varargs를 혼용하면 타입 안전성이 깨진다

static void dangerous(List<String>... stringLists) {
    List<Integer> intList = List.of(42);
    Object[] objects = stringLists;
    Object[0] = intList;                     // 힙 오염 발생
    String s = stringLists[0].get(0);   // ClassCastException
}

이렇게 타입안정성이 깨지니 제네릭 varargs 배열 매개변수에 값을 저장하는 것은 안전하지 않다.

@SafeVarargs
: 메서드 작성자가 그 메서드가 타입 안전함을 보장하는 장치
: 메서드가 안전한 게 확실하지 않다면 절대 사용해선 안 됨

안전한 제네릭 varargs 메서드는?

아래 두가지 조건을 충족하면 안전하다!

  1. varargs 매개변수 배열에 아무것도 저장하지 않는다.
  2. 그 배열(혹은 복제본)을 신뢰할 수 없는 코드에 노출하지 않는다.

추가적으로
@SafeVarargs는 재정의할 수 없는 매서드에만 달아야 한다. 재정의한 메서드도 안전할지는 보장할 수 없기 때문이다.
자바8에서는 오직 정적 메서드와 final 인스턴스 메서드에만 사용가능
자바9부터는 추가적으로 private 인스턴스 메서드에도 허용

@SafeVarargs가 유일한 정답은 아니다.
실체는 배열인 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);
        return result;
}

정적 팩터리 메서드인 List.of를 활용하면 이 메서드에 임의 개수의 인수를 넘길 수 있다. 이는 List.of에도 @SafeVarargs가 달려있기 때문!

audience = flatten(List.of(friends, romans, countrymen));

List.of 메소드의 주요 특징

  1. 불변성 : 생성된 리스트에 대한 어떠한 변형도 허용하지 않음.
  2. null 요소 불허 : List.of에 전달된 요소 중 하나라도 null이면 NullPointerException을 발생. 이는 리스트가 항상 null이 아닌 요소만 포함하도록 보장함.
  3. 직관적인 사용법 : 정적 메소드를 사용하기 때문에, 새로운 리스트를 생성하고 초기화하는 코드를 단순화
  4. 스레드 안전성 : 생성된 리스트는 변경할 수 없으므로 여러 스레드에서 동시에 접근해도 문제가 발생하지 않음

이 방식은 컴파일러가 메서드 타입 안정성을 검증할 수 있다는 장점이 있지만, 코드가 살짝 지저분해지며 속도가 조금 느려질 수 있다는 사소한(?) 단점이 존재함

🍑 결론

가변인수와 제네릭은 궁합이 좋지 않다.

제네릭 varargs 매개변수는 타입 안전하지는 않지만, 허용된다.
메서드에 제네릭 varargs 매개변수를 사용하고자 한다면, 먼저 그 메서드가 타입 안전한지 확인한 다음 @SafeVarargs 어노테이션을 달아 사용하는데 불편함이 없게끔 하자!


Referenced by

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions