Skip to content

Latest commit

 

History

History
168 lines (148 loc) · 8.04 KB

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

File metadata and controls

168 lines (148 loc) · 8.04 KB

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

가변인수 메서드

  • 가변인수는 메서드에 넘기는 인수의 개수를 클라이턴트가 조절할 수 있게 해줌
  • [타입... 변수명] 과 같은 형식으로 사용
  • 가변인수는 파라미터 중 가장 나중에 등장해야함
    static String varargsEx(String mail, String... str) {
        mail += " ";
        for (String s : str) {
            mail += s;
        }
        return mail;
    }

가변인수 메서드의 허점

  • 가변인수 메서드를 호출하면 가변인수를 담기 위한 배열이 자동으로 하나 만들어진다.
  • 그런데 가변 인수 구현 내부로 감춰야 할 것 같은 이 배열은 클라이언트에 노출된다.
  
public class Dangerous {  
  static void dangerous(List<String>... stringLists) {  
  Object[] objects = stringLists; // 클라이언트에게 노출되는 배열!
  ...
  • 그 결과 varargs 매개변수에 런타임에 실체화 되지 않는 것들이 넘어온다면 위험이 발생할 수 있다.
    • 제네릭 혹은 매개변수화 타입은 실체화되지 않는다
    • 제네릭 혹은 매개변수화 타입은 런타임에 컴파일타임보다 타입 관련 정보를 적게 가지고 있음 (소거)
public class Dangerous {  
  // 코드 32-1 제네릭과 varargs를 혼용하면 타입 안전성이 깨진다! (191-192쪽)  
  static void dangerous(List<String>... stringLists) {  
  List<Integer> intList = List.of(42);  
  Object[] objects = stringLists;  
  objects[0] = intList; // 힙 오염 발생  
  String s = stringLists[0].get(0); // 제네릭으로 인한 형변환 시도 but - ClassCastException  
  }  
  
  public static void main(String[] args) {  
  dangerous(List.of("There be dragons!"));  
  }
}
  • 이처럼 가변인수 List<String>... stringLists 처럼 제네릭 배열 매개변수에 값을 저장하는 것은 타입 안전하지 않다.

재미난 질문

  • 제네릭 배열을 프로그래머가 직접 생성하는 것은 허용되지 않는다.
	//컴파일 되지 않는다.
	List<String>[] stringLists = new List<String>[1];
  • 그런데 제네릭 varargs 매개변수를 받는 메서드를 선언할 수 있게 한 이유는 무엇일까?
public class Dangerous {  
  // 제네릭 varargs를 받는 모습 -> 제네릭 배열
  static void dangerous(List<String>... stringLists) {    
  }  
}
  • 제네릭 배열이 정말로 위험하다면 일관성이 떨어지는 부분처럼 보인다.
  • 자바 개발자들은 왜 이런 위험성을 남겨두었을까?
  • 답은 varargs 매개변수를 받는 메서드가 유용한 부분이 있기 때문
  • 그래서 언어 설계자들은 이러한 모순을 수용하기로 했다.
  • 실제로 이러한 모순적인 메서드는 여럿 제공되고 있음
Arrays.asList(T... a)
Collections.addAll(Collection<? super T> c, T... elements)
EnumSet.of(E first, E... rest)
  • 다행인 점은 이들은 타입 안전하다.

@SafeVarargs

  • 자바 7 전에는 제네릭 가변인수 메서드의 작성자가 호출자 쪽에서 발생하는 경고에 대해서 해줄 수 있는 일이 없었다.
  • 따라서 이런 메서드는 사용하기에 꺼림칙했고 사용자들은 이 경고들을 그냥 두거나@SuppressWarnings("unchecked") 애너테이션을 달아 경고를 숨겼다.
  • @SafeVarargs 애너테이션은 메서드 작성자가 그 메서드가 타입 안전함을 보장하는 장치다.
  • 컴파일러는 이 약속을 믿고 그 메서드가 안전하지 않을 수 있다는 경고를 더 이상 하지 않는다.
  • 따라서 메서드가 안전한 게 확실하지 않다면 절대 @SafeVarargs 애너테이션을 달아서는 안 된다.

varargs 매개변수 배열을 안전하게 사용하는 방법

  • varargs 매개변수 배열이 호출자로부터 그 메서드로 순수하게 인수들을 전달하는 일만 한다면 그 메서드는 안전하다.
  • varargs의 목적대로만 쓰인다면 안전하다는 것이다.

varargs매개변수 배열이 안전하지 않은 경우

  • 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에 넘길 T 인스턴스 2개를 담을 varargs 매개변수 배열을 만드는 코드를 생성한다.
  • pickTwo메서드가 만드는 배열의 타입은 항상 Object[] 타입 배열이다.
  • pickTwo에 어던 타입의 객체를 넘기더라도 담을 수 있는 가장 구체적인 타입이 Object이기 때문
public static void main(String[] args) {
	String[] attributes = pickTwo("좋은", "빠른", "저렴한");
}
  • pickTwo를 사용하는 클라이언트 코드를 보자
  • 별다른 경고 없이 컴파일 되지만 실행하면 ClassCastException을 던진다.
  • 형변환하는 코드를 컴파일러가 자동 생성할 때 Object[]는 String[]의 하위타입이 아니기 때문에 형변환에 실패하는 것이다.
  • 이 실패가 다소 황당하게 느껴질 수 있을 것이다.
  • 힙 오염을 발생시킨 진짜 원인인 toArray로부터 두 단계나 떨어져 있고, varargs 매개변수 배열은 실제 매개변수가 저장된 후로 변경된 적이 없으니 말이다.
  • 이 예는 제네릭 varargs 매개변수가 배열에 다른 메서드가 접근하도록 허용하면 안전하지 않다라는 점을 시사한다.
  • 단 예외가 두가지 있다.
    • @SafeVarargs로 제대로 애노테이트 된 또 다른 메서드에 넘기는 경우 안전
    • 배열 내용의 일부 함수를 호출만 하는 (varargs를 직접 참조하지 않는) 일반 메서드에 넘기는 경우 안전

제네릭 varargs 매개변수를 안전하게 사용하는 전형적인 예시

@SafeVarargs
static <T> List<T> flatten(List<? extends T>... lists) {
	List<T> result = new ArrayList<>();
	for (List<? extends T> list : lists) {
		result.addAll(list);
	}
}
  • 위의 코드는 모든 원소를 하나의 리스트로 옮겨 담아 반환
  • @SafeVarargs 애너테이션이 달려 있으니 선언하는 쪽과 사용하는 쪽 모두에서 경고를 내지 않음

@SafeVarargs 애너테이션 규칙

  • 제네릭이나 매개변수화 타입의 varargs 매개변수를 받는 모든 메서드에 @SafeVarargs를 달라.
  • 그래야 사용자를 헷갈리게 하는 컴파일러 경고를 없앨 수 잇다.
  • 이 말은 안전하지 않은 varargs 메서드는 절대 작성해서는 안된다는 뜻
  • 힙 오염 경고가 뜨는 메서드가 있다면, 그 메서드가 진짜 안전한지 점검하라

@SafeVarargs를 통해 안전해진 예시

  • 다음의 코드는 안전할까 안전하지, 않을까
flatten(List.of(friends, romans, coutrymen), List.of(wo, wa, course));
  • 안전하다.
  • 안전한 이유는 List.of에 @SafeVarargs가 달려있기 때문
  • 미리 구현된 @SafeVarargs 애너테이션을 우리가 직접 달지 않아도 되며, 실수로 안전하다고 판단할 걱정도 없다.
  • 이 방식을 안전하지 않았던 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);
	}
}