[Effective Java] - 5장 아이템 32. 제네릭과 가변인수를 함께 쓸 때는 신중하라

yeom yaloo·2024년 1월 3일
0

Effective Java

목록 보기
20/20
post-thumbnail

제네릭

[아이템 32. 제네릭과 가변인수를 함께 쓸 때는 신중하라]

[가변인수]

0. 가변인수

public void method(String... str){
	//생략
}
  • ... 을 사용하여 메서드의 parameter를 구성하는 코드가 가변인수를 정의하는 코드이다.

1. 가변인수의 등장

  • 가변인수(varargs)는 제네릭과 함께 자바 5에서 등장한 개념이다.

2. 가변인수란?

  • 메서드에 넘기는 인수의 개수를 클라이언트가 조절할수 있게 하는 인수이다.

3. 가변인수 메서드의 호출

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

4. 가변인수의 허점

  • 내부로 해당 정보를 감춰야 하는데 가변인수를 사용할 경우엔 가변인수를 담기 위한 배열을 클라이언트에게 그대로 노출한다는 문제점이 발생한다.

[가변인수로 실체화 불가 타입을 사용할 때]

1. 실체화 불가 타입

  • E,List<E>,List<String> 등을 실체화 불가 타입이라고 하는데 해당 타입을 가변 인수로 넘겨주면 컴파일러가 경고를 날린다.
  • 실체화 불가 타입은 런타임시 컴파일타임보다 타입 관련 정보를 적게 담고 있다. 제네릭과 같은 실체화 불가 타입은 컴파일 시 타입을 벗겨내기 때문이다.
  • 또한 제네릭과 매개변수화 타입은 대게 대부분이 실체화되지 않는다.

2. 매개변수화 타입 변수가 다른 객체를 참조할 경우엔 힙 오염이 발생한다.

static void dangerous(List<String>... stringLists){
	List<Integer> intList = List.of(42);
	Object[] objects = stringLists;
    
    /*
    	매개변수화 타입인 intList가 Object[] 배열을 참조해버리면
        힙 오염이 발생하게 된다.
    */
    obcts[0] = intList; // 힙오염 발생
    String s = stringLists[0].get(0); // ClassCastException
  • 매개변수화 타입의 변수가 타입이 다른 객체를 참조하면 힙 오염이 발생한다.
  • 이는 제네릭으로 약속한 타입 시스템 안전성의 근간을 흔들어버리는 일이다.

[실무에선 유용한 가변인수로 실체화 불가 타입을 사용하는 것]

1. 경고로 끝나는 오류

  • 위의 가변인수로 매개변수화 타입을 넣는 행위가 경고로만 끝나고 있다. 왜일까?
  • 이는 제네릭이나 매개변수화 타입의 가변인수 매개변수를 받는 메서드 가실무에서는 매우 유용하기 때문이다.
  • 그렇기 때문에 설계자가 이 모순을 받아들이기로 했다.

2. 모순을 받아들여 적용된 라이브러리들

  • Arrays.asList(T... a)
  • Collections.addAll(Collection<? super T> c, T... elements)
  • EnumSet.of(E first, E... rest)
  • 위의 보여준 예시와 달리 해당 라이브러리들의 타입은 안전하다.

[@SafeVarargs]

1. 자바 7전의 상황

  • 자바 7 전에는 제네릭 가변인수 메서드의 작성자가 호출자 쪽에서 발생하는 경고에 대해서 해줄 수 있는 일이 없었다.
  • 그래서 단순히 @SuppressWarnings("unchecked") 애너테이션을 달아서 경고를 숨겨야 했다.
  • 때로는 이 작업이 진짜 문제를 알려주는 경고를 숨겨 안 좋은 결과로 이어졌다.

2. 경고를 숨기는 @SafeVarargs

  • 자바 7부터는 해당 애너테이션이 추가 되어, 클라이언트 측에서 발생하는 경고를 숨길 수 있게 됐다.
  • 해당 애너테이션은 메서드 작성자가 그 메서드가 타입 안전성을 보장하는 장치로 사용한다.
  • 주의점은 해당 메서드가 안전하지 않다면 @SafeVarargs 애너테이션을 달지 않아야 한다.

3. 가변인수에 매개변수화 타입을 사용해도 타입 안전성을 보장할 수 있는 경우

  • 가변인수 메서드를 호출하면 가변인수를 담을 배열이 생성됨을 일단 기억하자.
  • 메서드가 이 배열에 아무것도 저장하지 않고, 그 배열의 참조가 밖으로 노출되지 않는다면 타입이 안전하다고 할수 있다.
  • 매개변수 배열이 호출자로부터 그 메서드로 순수하게 인수들을 전달하는 일만 한다면 그 메서드는 안전하다.

[가변인수 사용에도 메서드가 안전함을 확인하는 방법]

1. 안전하지 않은 경우

1-1. 자신의 제네릭 매개변수 배열의 참조를 노출하는 경우

static <T> T[] toArray(T... args){
	return args;//자기 자신의 제네릭 매개변수 배열의 참조를 노출
}
  • 배열의 타입은 컴파일 타임에 결정되는데, 그 시점에 잘못 판단할 수 있다.
  • varargs 매개변수 배열을 그대로 반환할 경우 힙 오염을 이 메서드를 호출한 쪽의 콜스택으로까지 전이하는 결과가 생길 수 있다.

1-2. 매개변수 배열에 다른 변수가 접근하는 경우

static <T> T[] pickTwo(T a, T b, T c){
	switch(ThreadLocalRandom.current().nextInt(3)){
    // 0 ~ 2까지의 랜덤하게 수를 뽑아 case문을 동작시키는 코드
    	case 0: return toArray(a,b);
        case 1: return toArray(a,c);
        case 2: return toArray(b,c);
    }
}
//위의 static 메서드를 메인 메서드에서 사용하는 코드
public static void main(String[] args){
	String[] attributes = pickTwo("좋은", "빠른", "저렴한");
}
  • 위의 메서드는 제네릭 가변인수를 받는 toArray()를 호출하고 있다.
  • 이 코드가 만들 배열의 타입은 Object[]이고 main 메서드에서 사용할 땐 String[]으로 형변환해서 받아온다.
  • 이때 String[]으로 자동 형변환 하는 코드가 컴파일러에서 자동 생성하고 이는 Object[]가 String[]의 하위 타입이 아니여서 문제가 생기게 된다.

2. 안전하게 사용한 경우

@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 애너테이션을 달아주었기 때문에 선언하는 쪽과 사용하는 쪽 모두에서 경고를 내지 않는다.

[@SafeVarargs와 관련된 여러 경우]

1. @SafeVarargs를 사용하지 않아야 할 두가지 경우

  1. varargs 매개변수 배열에 아무것도 저장하지 않는다.
  2. 그 배열(혹은 복제본)을 신뢰할 수 없는 코드에 노출하지 않는다.
  3. 재정의할 수 있는 메서드에는 해당 애너테이션을 달수 없다. (재정의 메서드 역시 안전을 보장할 수 없기 때문이다.)

2. @SafeVarargs를 대체하는 방법

static <T> List<T> flatten(List<List<? extends ?>> lists){
	List<T> result = new ArrayList<>();
    for (List<? extends T> list: lists)
    	result.addAll(list);
    return result;
}
  • 속도가 살짝 느려지고, 코드가 지저분해진다는 단점이 있지만 실수로 안전하다고 판단할 일이 없어진다.
profile
즐겁고 괴로운 개발😎

0개의 댓글