Effective Java 3rd - Ch05
포스트
취소

Effective Java 3rd - Ch05

Java 기초를 다지기 위해 효과적인 자바란 책을 공부 중입니다.

제5장 제네릭

  • 제네릭은 컬렉션이 담을 수 있는 타입을 컴파일러에 알려주게 된다
  • 엉뚱한 타입의 객체를 넣으려는 시도를 컴파일 과정에서 차단
  • 코드가 복잡해진다는 단점
  • 제네릭의 이점을 최대로 살리고 단점을 최소화하는 방법을 소개

26) RAW 타입은 사용하지 말라

제네릭 클래스와 제네릭 인터페이스를 통틀어 제네릭 타입이라고 함

  • 제네릭 타입은 일련의 매개변수화 타입을 정의한다, ex) List<String>
  • 제네릭 타입을 정의하면 그에 딸린 raw 타입도 함께 정의된다
    • ex) List<E> 의 raw 타입은 List
  • 제네릭 타입은 형변환에 있어 절대 실패하지 않음을 보장
    • 컴파일러는 컬렉션에서 원소를 꺼내는 모든 곳에 보이지 않는 형변환을 추가
  • 호환성 문제 때문에 매개변수가 없는 raw 타입을 남겨두긴 했지만 절대 써서는 안됨

코드 26-3 매개변수화 된 컬렉션 타입 - 타입 안정성 확보

1
private final Collection<Stamp> stamp = ...;
1
- List<Object> 같은 매개변수화 타입을 사용할 때와 달리 List 같은 raw 타입을 사용하면 타입 안정성을 잃게 된다

코드 26-4 런타임에 실패한다 - unsafeAdd 메서드가 raw 타입(List)을 활용

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public static void main(String[] args) {
  List<String> strings = new ArrayList<>();
  unsafeAdd(strings, Integer.valueOf(42));
  String s = strings.get(0);    // 컴파일러가 자동으로 형변환 코드를 넣어준다
}

private static void unsafeAdd(List list, Object o) {
  list.add(o);
}

/*
// 컴파일은 되지만 다음과 같은 경고가 발생한다
Test.java:10: warning: [unchecked] unchecked call to add(E) as a memeber of the raw type List
  --> list.add(o);

// List를 List<Object>로 바꾸고 컴파일하면 컴파일 조차 되지 않는다
Test.java:5: error: incompatible types: List<String> cannot be converted to List<Object>
  --> unsafeAdd(strings, Integer.valueOf(42));
*/

코드 26-5 잘못된 예 - 모르는 타입의 원소도 받는 raw 타입을 사용했다

1
2
3
4
5
6
7
8
// 동작은 하지만 raw 타입을 사용해 안전하지 않다
static int numElementsInCommon(Set s1, Set s2) {
  int result = 0;
  for( Object o1 : s1 )
    if( s2.contains(o1) )
      result++;
  return result;
}

이런 경우엔 와일드카드 타입을 사용하는게 좋다. :arrow_right: Set>

  • Collection<?> 에는 (null 외에는) 어떤 원소도 넣을 수 없다
  • 어쨌든 컬렉션의 타입 불변식을 훼손하지 못하게 막는다
  • 이러한 제약을 받아들 수 없다면 제네릭 메서드나 한정적 와일드카드 타입을 사용하면 된다

코드 26-6 비한정적 와일드카드 타입을 사용하라 - 타입에 안전하며 유연하다

1
static int numElementsInCommon( Set<?> s1, Set<?> s2 ){ ... }

코드 26-7 raw 타입을 써도 좋은 예 - instanceof 연산자

1
2
3
4
// o의 타입이 Set 임을 확인한 다음 Set<?> 로 형변환해야 한다
if( o instanceof Set ){     // raw 타입
  Set<?> s = (Set<?>) o;    // 와일드카드 타입 (컴파일러 경고가 뜨지 않는다)
}

:bangbang:   용어 정리

한글용어 영문용어 관련항목
매개변수화 타입 parameterized type List<String> 항목26
실제 타입 매개변수 actual type parameter String 항목26
제네릭 타입 generic type List<E> 항목26,29
정규타입 매개변수 formal type parameter E 항목26
비한정적 와일드카드 타입 unbounded wildcard type List<?> 항목26
로 타입 raw 타입 List 항목26
한정적 타입 매개변수 bounded type parameter <E extends Number> 항목29
재귀적 타입 한정 recursive type bound <T extends Comparable<T>> 항목30
한정적 와일드카드 타입 bounded wildcard type List<? extends Number> 항목템31
제네릭 메서드 generic method static <E> List<E> asList(E[] a) 항목30
타입 토큰 type token String.class 항목33

27) 비검사 경고를 제거하라

제네릭을 사용하기 시작하면 수많은 컴파일러 경고를 보게 될 것이다.

1
2
3
4
5
6
// unchecked conversion 경고가 뜬다
Set<Lark> exaltation = new HashSet();

// **해결 ==>
// 자바 7부터 지원하는 다이아몬드 연산자(<>)만으로 해결할 수 있다
Set<Lark> exaltation = new HashSet<>();
1
- 할 수 있는 한 모든 비검사 경고를 제거하라!

경고를 제거할 수는 없지만 타입 안전하다고 확신할 수 있다면

  • @SuppressWarnings(“unchecked”) 애너테이션을 달아 경고를 숨기자
  • @SuppressWarnings 애너테이션은 항상 좁은 범위에 적용하자
  • @SuppressWarnings 애너테이션을 사용할 때면 그 이유를 항상 주석으로 남겨야 한다

코드 27-1 지역변수를 추가해 @SuppressWarnings 의 범위를 좁힌다

1
2
3
4
5
6
7
8
9
10
11
12
13
public <T> T[] toArray(T[] a) {
  if( a.length < size ){
    // 생성한 배열과 매개변수로 받은 배열의 타입이 모두 T[] 로 같으므로
    // 올바른 형변환이다
    @SuppressWarnings("unchecked") T[] result =
        (T[]) Arrays.copyOf(elements, size, a.getClass());
    return result;
  }
  System.arraycopy(elements, 0, a, 0, size);
  if( a.length > size )
    a[size] = null;
  return a;
}

28) 배열보다는 리스트를 사용하라

배열과 제네릭 타입의 중요한 차이 2가지

  • 배열은 공변(covariant)이다. 반면 제네릭은 불공변
    • 배열 : Sub 가 Super 의 하위타입이라면 배열 Sub[] 는 배열 Super[] 의 하위타입
    • 제네릭 : Type1 과 Type2 가 있을 때, List<Type1> 은 List<Type2> 와 호환되지 않는다
  • 배열은 실체화(reify) 된다
    • 배열은 런타임에도 자신이 담기로 한 원소의 타입을 인지하고 확인한다 (코드 28-1)
    • 제네릭은 타입 정보가 런타임에는 소거된다 (컴파일 타임에만 검사)

코드 28-1 런타임에 실패한다

1
2
Object[] objectArray = new Long[1];
objectArray[0] = "타입이 달라 넣을 수 없다";    // ArrayStoreException

코드 28-2 컴파일이 되지 않는다

1
2
List<Object> ol = new ArrayList<Long>();    // 호환되지 않는 타입
ol.add("타입이 달라 넣을 수 없다");

배열과 제네릭은 잘 어울어지지 못하며, 타입 안전을 위해 제네릭 배열을 만들지 못하게 막았다.

코드 28-3 제네릭 배열 생성을 허용하지 않는 이유 - 컴파일 되지 않는다

1
2
3
4
5
6
7
List<String>[] stringLists = new List<String>[1];   // 이것이 허용된다면??
Object[] objects = stringLists;

List<Integer> intList = List.of(42);
objects[0] = intList;               // 뒤죽박죽

String s = stringLists[0].get(0);   // List<String>이 아니라 List<Integer> 가 꺼내짐

생성자에서 컬렉션을 받는 Chooser 클래스를 예로 살펴보자

코드 28-4 Chooser - 제네릭을 시급히 적용해야 한다

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Chooser {
  private final Object[] choiceArray;

  public Chooser(Collection choices) {
    choiceArray = choices.toArray();
  }

  // choose 호출시마다 반환된 Object를 형변환해야 한다
  public Object choose() {
    Random rnd = ThreadLocalRandom.current();
    return choiceArray[rnd.nextInt(choiceArray.length)];
  }
}

코드 28-5 Chooser 를 제네릭으로 만들기 위한 첫 시도 - 컴파일 되지 않는다

1
2
3
4
5
6
7
8
9
10
11
12
public class Chooser<T> {
  private final T[] choiceArray;

  public Chooser(Collection<T> choices) {
    choiceArray = choices.toArray();           // <<-- 1) 컴파일 오류
    // 1) 오류로 타입 캐스팅을 했으나
    // choiceArray = (T[]) choices.toArray();  // <<-- 2) 형변환 경고
    // 2) 경고에도 불구하고 동작은 한다. 그러나 타입 안전은 보장 못함
  }

  ... // choose 메서드는 그대로
}

코드 28-6 리스트 기반 Chooser - 타입 안전성 확보

1
2
3
4
5
6
7
8
9
10
11
12
public class Chooser<T> {
  private final List<T> choiceList;

  public Chooser(Collection<T> choices) {
    choiceList = new ArrayList<>(choices);
  }

  public T choose() {
    Random rnd = ThreadLocalRandom.current();
    return choiceList.get(rnd.nextInt(choiceList.size()));
  }
}

코드의 양은 조금 늘었고 아마도 조금 더 느릴 테지만, 그만한 가치가 있다.

29) 이왕이면 제네릭 타입으로 만들어라

어렵지만 배워두면 그만한 값어치는 충분히 한다.

코드 29-1 Object 기반 스택 - 제네릭이 절실한 강력 후보

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class Stack {
  private Object[] elements;
  private int size = 0;
  private static final int DEFAULT_INITIAL_CAPACITY = 16;

  public Stack() {
    elements = new Object[DEFAULT_INITIAL_CAPACITY];
  }

  public void push(Object e) {
    ensureCapacity();
    elements[size++] = e;
  }

  public Object pop() {
    if( size == 0 ) throw new EmptyStackException();
    Object result = elements[--size];
    elements[size] = null;    // 다 쓴 참조 해제
    return result;
  }

  public boolean isEmpty() {
    return size == 0;
  }

  // 원소를 위한 공간을 적어도 하나 이상 확보한다
  // 배열 크기를 늘려야 할 때마다 대략 두 배씩 늘린다
  private void ensureCapacity() {
    if( elements.length == size )
      elements = Arrays.copyOf(elements, 2*size + 1);
  }
}

코드 29-2 제네릭 스택으로 가는 첫 단계 - 컴파일 되지 않는다

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class Stack<E> {
  private E[] elements;
  private int size = 0;
  private static final int DEFAULT_INITIAL_CAPACITY = 16;

  public Stack() {
    // error : generic array creation
    elements = new E[DEFAULT_INITIAL_CAPACITY];

    // warning : 이렇게 고칠 수도 있으나 타입 안전하지 않다
    // elements = (E[]) new Object[DEFAULT_INITIAL_CAPACITY];
  }

  public void push(E e) {
    ensureCapacity();
    elements[size++] = e;
  }

  public E pop() {
    if( size == 0 ) throw new EmptyStackException();
    E result = elements[--size];
    elements[size] = null;    // 다 쓴 참조 해제
    return result;
  }

  ... // 동일 코드 생략
}

해결책은 두가지

  • 첫번째는 제네릭 배열 생성을 금지하는 제약을 대놓고 우회하는 방법
    • Object[] 로 생성하지만 제네릭 배열로 형변환을 하여 사용
    • :arrow_right: 가독성이 더 좋다, 코드도 짧다 (생성할 때만 형변환)
    • :arrow_right: 런타임 타입이 컴파일 타임 타입과 달라 힙 오염을 일으킨다
  • 두번째는 elements 필드의 타입을 E[] 에서 Object[] 로 바꾸는 것
    • Object[] 에 저장하고 push, pop 등 사용할 때마다 형변환을 하여 사용
    • :arrow_right: 힙 오염은 없다

코드 29-3 배열을 사용한 코드를 제네릭으로 만드는 방법 1

1
2
3
4
5
6
7
// 배열 elements 는 push(E) 로 넘어온 E 인스턴스만 담는다
// 따라서 타입 안전성을 보장하지만,
// 이 배열의 런타임 타입은 E[] 가 아닌 Object[] 이다
@SuppressWarnings("unchecked")
public Stack() {
  elements = (E[]) new Object[DEFAULT_INITIAL_CAPACITY];
}

코드 29-4 배열을 사용한 코드를 제네릭으로 만드는 방법 2

1
2
3
4
5
6
7
8
9
10
11
// 비검사 경고를 적절히 숨긴다
public E pop() {
  if( size == 0 ) throw new EmptyStackException();

  // push 에서 E 타입만 허용하므로 이 형변환은 안전하다
  @SuppressWarnings("unchecked")
  E result = (E) elements[--size];

  elements[size] = null;    // 다 쓴 참조 해제
  return result;
}

코드 29-5 제네릭 Stack 을 사용하는 맛보기 프로그램

1
2
3
4
5
6
7
8
// 제네릭 stack 사용시 명시적 형변환이 없다 (컴파일러에 의해 수행)
publi static void main(String[] args) {
  Stack<String> stack = new Stack<>();
  for( String arg : args )
    stack.push( arg );
  while( !stack.isEmpty() )
    System.out.println(stack.pop().toUpperCase());
}

30) 이왕이면 제네릭 메서드로 만들어라

참고: 이것이 자바다 2

제네릭 메소드는 매개변수 타입과 리턴 타입으로 타입 파라미터를 갖고 있는 메소드를 말합니다. 리턴 타입 앞에 “<>” 부호를 추가하고 사이에 타입 파라미터를 기술하면 됩니다. 그런 다음 리턴 타입과 매개변수 타입으로 타입 파라미터를 사용하면 됩니다.

1
public <타입파라미터,..> 리턴타입 메소드이름(매개변수,...) { ... }

제네릭 메소드를 사용하는 방법으로는 두 가지가 있습니다.

  • (1) 명시적으로 구체적 타입을 지정하는 방법
    • <구체적타입>메소드이름(매개값)
    • :arrow_right: Set aflCio = union( guys, stooges );
  • (2) 매개 값을 보고 구체적 타입을 추정하는 방법
    • 메소드이름(매개값)
    • :arrow_right: Set aflCio = union( guys, stooges );

클래스와 마찬가지로, 메서드도 제네릭으로 만들 수 있다.

코드 30-1 raw 타입 사용 - 수용 불가

1
2
3
4
5
6
// 경고를 없애려면 이 메서드를 타입 안전하게 만들어야 한다
public static Set union(Set s1, Set s2) {
  Set result = new HashSet(s1);     // unchecked 경고 발생
  result.addAll(s2);                // unchecked 경고 발생
  return result;
}

(타입 매개변수들을 선언하는) 타입 매개변수 목록은 메서드의 제한자와 반환 타입 사이에 온다.

코드 30-2 제네릭 메서드

1
2
3
4
5
public static <E> Set<E> union(Set<E> s1, Set<E> s2) {
  Set<E> result = new HashSet(s1);
  result.addAll(s2);
  return result;
}

코드 30-3 제네릭 메서드를 활용하는 간단한 프로그램

1
2
3
4
5
6
public static void main(String[] args) {
  Set<String> guys = Set.of("톰", "딕", "해리");
  Set<String> stooges = Set.of("래리", "모에", "컬리");
  Set<String> aflCio = union( guys, stooges );    // 추정에 의한 사용방법(2)
  System.out.println(aflCio);
}

항등함수란, 입력값을 수정없이 그대로 반환하는 특별한 함수

  • 항등함수 객체는 상태가 없으니 요청할 때마다 새로 생성하는 것은 낭비다
  • 자바 라이브러리의 Function.identify 를 사용하면 되지만 연습해보자
  • 제네릭 싱글턴 팩토리를 이용하면 된다
    • 요청한 타입 매개변수에 맞게 맵번 그 객체의 타입을 바꿔주는 정적 팩토리

코드 30-4 제네릭 싱글턴 팩토리 패턴

1
2
3
4
5
6
private static UnaryOperator<Object> ITENTIFY_FN = (t) -> t;

@SuppressWarnings("unchecked")
public static <T> UnaryOperator<T> identifyFunction() {
  return (UnaryOperator<T>) IDENTIFY_FN;    // 비검사 형변환 경고 발생
}

코드 30-5 제네릭 싱글턴을 사용하는 예

1
2
3
4
5
6
7
8
9
10
11
12
// 왜 이렇게 하는지는 모르겠음 (?) : 함수형 프로그래밍 관련
public static void main(String[] args) {
  String[] strings = { "하나", "둘", "셋" };
  UnaryOperator<String> sameString = identifyFunction();
  for( String s : strings )
    System.out.println( sameString.apply(s) );

  Number[] numbers = { 1, 2.0, 3L };
  UnaryOperator<Number> sameNumber = identifyFunction();
  for( Number n : numbers )
    System.out.println( sameNumber.apply(n) );
}

:hand: 참고 : 함수형 인터페이스 UnaryOperator 와 추상 메소드 apply

재귀적 타입 한정

  • 드물긴 하지만, 자기 자신이 들어간 표현식을 사용하여 타입 매개변수의 허용 범위를 한정할 수 있다
  • 주로 타입의 자연적 순서를 정하는 Comparable 인터페이스와 함께 쓰인다

코드 30-6 재귀적 타입 한정을 이용해 상호 비교할 수 있음을 표현했다

1
2
3
4
5
6
public interface Comparable<T> {
  int compareTo(T o);
}

// <E extends Comparable<E>> 는 '모든 타입 E는 자신과 비교할 수 있다'라는 의미
public static <E extends Comparable<E>> E max(Collection<E> c);

코드 30-7 컬렉션에서 최대값을 반환한다 - 재귀적 타입 한정 사용

1
2
3
4
5
6
7
8
9
10
11
12
13
// 코드 30-6 에서 선언한 함수의 구현
public static <E extends Comparable<E>> E max(Collection<E> c) {
  // 팁: 예외를 던지는 것보다 Optional<E> 를 반환하도록 고치는 것이 낫다
  if( c.isEmpty() )
    throw new IllegalArgumentException("컬렉션이 비어 있습니다");

  E result = null;
  for( E e : c )
    if( result == null || e.compareTo(result) > 0 )
      result = Objects.requireNonNull(e);

  return result;
}

31) 한정적 와일드 카드를 사용해 API 유연성을 높여라

코드 31-1 와일드카드 타입을 사용하지 않은 pushAll 메서드 - 결함이 있다

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Stack<E> {
  // 동일 코드 생략
  public Stack();
  public void push(E e);
  public E pop();
  public boolean isEmpty();

  public void pushAll(Iterable<E> src) {
    for( E e : src )
      push(e);
  }
}

// 만약 Number 타입에 대해 Integer 가 들어오면 어떻게 될까?
// ==> 매개변수화 타입이 불공변이기 때문에 오류 발생
Stack<Number> numberStack = new Stack<>();
Iterable<Integer> integers = ...;
numberStack.pushAll( integers );

Number 의 하위 타입인 Integer 등에 대해 Iterable 선언을 하기 위해 와일드카드 한정 타입을 사용한다

코드 31-2 E 생산자(producer) 매개변수에 와일드카드 타입 적용

1
2
3
4
public void pushAll(Iterable<? extends E> src) {
  for( E e : src )
    push(e);
}

코드 31-3 와일드카드 타입을 사용하지 않은 popAll 메서드 - 결함이 있다

1
2
3
4
5
6
7
8
9
10
public void popAll(Collection<E> dst) {
  while( !isEmpty() )
    dst.add( pop() );
}

// Stack<Number> 의 원소를 Object용 컬렉션으로 옮기려 한다면?
// ==> 비슷한 오류 발생
Stack<Number> numberStack = new Stack<>();
Collection<Object> objects = ...;
numberStack.popAll( objects );

코드 31-4 E 소비자(consumer) 매개변수에 와일드카드 타입 적용

1
2
3
4
public void popAll(Collection<? super E> dst) {
  while( !isEmpty() )
    dst.add( pop() );
}

유연성을 극대화하려면 원소의 생산자나 소비자용 입력 매개변수에 와일드카드 타입을 사용하라

PECS 공식 : producer-extends, consumer-super

코드 31-5 T 생산자 매개변수에 와일드카드 타입 적용

1
2
3
4
5
6
7
8
9
10
// 항목28 의 예제의 적용 예
public Chooser( Collection<? extends T> choices );

// 반환 타입은 여전히 Set<E>
// ==> 반환 타입에는 한정적 와일드카드 타입을 사용하면 안된다
public static <E> Set<E> union( Set<? extends E> s1, Set<? extends E> s2 );

Set<Integer> integers = Set.of(1, 3, 5);
Set<Double> doubles = Set.of(2.0, 4.0, 6.0);
Set<Number> numbers = union(integers, doubles);   // 자동 형변환(신기!)

클래스 사용자가 와일드카드 타입을 신경 써야 한다면 그 API에 무슨 문제가 있을 가능성이 크다.

코드 31-6 자바 7까지는 명시적 타입 인수를 사용해야 한다

1
2
// 목표 타이핑은 자바 8부터 지원하기 시작
Set<Number> numbers = Union.<Number>union(integers, doubles);
1
2
3
// 코드 30-7 의 max 메서드 사례
public static <E extends Comparable<? super E>> E max(  // 출력변수 : 소비하는 쪽
          List<? extends E> list );                     // 입력변수 : 생산하는 쪽

코드 31-7 swap 메서드의 두가지 선언

1
2
3
// 메서드를 정의할 때 둘 중 어느 것을 사용해도 괜찮을 때가 있다
public static <E> void swap(List<E> list, int i, int j);  // 비한정적 타입 매개변수 사용
public static void swap(List<?> list, int i, int j);      // 비한정적 와일드카드 사용

규칙 : 메서드 선언에 타입 매개변수가 한번만 나오면 와일드카드로 대체하라

1
2
3
4
5
// 이해하기 어려운 오류가 발생한다
// ==> 리스트 타입이 List<?> 인데 null 외에는 어떤 값도 넣을 수 없기 때문
public static void swap(List<?> list, int i, int j) {
  list.set(i, list.set(j, list.get(i)));
}

실제 타입을 알아내려면 도우미 메서드는 제네릭 메서드여야 한다.

1
2
3
4
5
6
7
8
9
public static void swap(List<?> list, int i, int j) {
  swapHelper(list, i, j);
}

// 와일드카드 타입을 실제 타입으로 바꿔주는 private 도우미 메서드
// swapHelper 메서드는 리스트가 List<E> 임을 알고 있다
private static <E> void swapHelper(List<E> list, int i, int j) {
  list.set(i, list.set(j, list.get(i)));
}

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

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

가변인수(varargs) 기능은 배열을 노출하여 추상화가 완벽하지 못하고, 배열과 제네릭의 타입 규칙이 서로 다르기 때문이다. 이로 인해 타입 안정성이 깨지니 제네릭 varargs 배열 매개변수에 값을 저장하는 것은 안전하지 않다.

코드 32-1 제네릭과 varargs 를 혼용하면 타입 안전성이 깨진다

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

자바 7에서는 @SafeVarargs 애너테이션이 추가되어 제네릭 가변인수 메서드 작성자가 클라이언트 측에서 발생하는 경고를 숨길 수 있게 되었다.

코드 32-2 자신의 제네릭 매개변수 배열의 참조를 노출한다 - 안전하지 않다

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
static <T> T[] toArray(T... args) {
  return args;
}

/////////////////////////

static <T> T[] pickTwo(T a, T b, T c) {
  switch( ThreadLocalRandom.current().nextInt(3)) {
    // 컴파일러는 toArray 에게 전달될 varargs 매개변수 배열을 만드는 코드를 생성한다
    // --> 무엇이든 담을 수 있도록 Object[] 가 생성됨
    case 0: return toArray(a, b);
    case 1: return toArray(a, c);
    case 2: return toArray(b, c);
  }
  throw new AssertionError();     // 도달할 수 없다
}

/////////////////////////

public static void main(String[] args) {
  String[] attributes = pickTwo("좋은", "빠른", "저렴한");
}

이 메서드가 반환하는 배열의 타입은 인수를 넘기는 컴파일타임에 결정되는데, 그 시점에서 컴파일러에게 충분한 정보가 주어지지 않아 타입을 잘못 판단할 수 있다. 제네릭 varargs 매개변수 배열에 다른 메서드가 접근하도록 허용하면 안전하지 않다.

코드 32-3 제네릭 varargs 매개변수를 안전하게 사용하는 메서드

1
2
3
4
5
6
7
@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;
}

제네릭이나 매개변수화 타입의 varargs 매개변수를 받는 모든 메서드에 @SafeVarargs 를 달라. 또한 @SafeVarargs 애너테이션은 재정의 할 수 없는 메서드에만 달아야 한다. (자바 8에서 이 애너테이션은 오직 정적 메서드와 final 인스턴스 메서드에만 붙일 수 있다)

:bangbang:   두 조건을 만족하는 제네릭 varargs 메서드는 안전하다

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

코드 32-4 제네릭 varargs 매개변수를 List 로 대체한 예 - 타입 안전하다

1
2
3
4
5
6
7
8
9
10
11
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 에도 @SafeVarargs 애너테이션이 달려 있다
audience = flatten(List.of(friends, romans, countrymen));

이 방식의 장점은 컴파일러가 이 메서드의 타입 안전성을 검증할 수 있다는데 있다.

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) {
  switch( ThreadLocalRandom.current().nextInt(3)) {
    // 컴파일러는 toArray 에게 전달될 varargs 매개변수 배열을 만드는 코드를 생성한다
    // --> 무엇이든 담을 수 있도록 Object[] 가 생성됨
    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("좋은", "빠른", "저렴한");
}

결과 코드는 배열 없이 제네릭만 사용하므로 타입 안전하다.

33) 타입 안전 이종 컨테이너를 고려하라

타입 한정이 아닌, 더 유연한 수단이 필요할 때도 종종 있다. 컨테이너 대신 키를 매개변수화 한 다음, 컨테이너에 값을 넣거나 뺄 때 매개변수화 한 키를 함게 제공하면 된다. 이러한 설계 방식을 타입안전 이종 컨테이너 패턴이라 한다.

코드 33-1 타입 안전 이종 컨테이너 패턴 - API

1
2
3
4
public class Favorites {
  public <T> void putFavorites(Class<T> type, T instance);
  public <T> T getFavorites(Class<T> type);
}

코드 33-2 타입 안전 이종 컨테이너 패턴 - 클라이언트

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static void main(String[] args) {
  Favorites f = new Favorites();

  f.putFavorites(String.class, "Java");
  f.putFavorites(Integer.class, 0xcafebabe);
  f.putFavorites(Class.class, Favorites.class);

  String favoriteString = f.getFavorites(String.class);
  int favoriateInteger = f.getFavorites(Integer.class);
  Class<?> favoriateClass = f.getFavorites(Class.class);

  // 자바의 printf 함수에서 '%n'은 플랫폼에 맞는 줄바꿈 문자로 자동 대치된다
  System.out.printf("%s %x %s%n", favoriteString, favoriateInteger, favoriateClass.getName());
}
// 출력 --> Java cafebabe Favorites

코드 33-3 타입 안전 이종 컨테이너 패턴 - 구현

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Favorites {
  // 비한정적 와일드카드로 키를 정의하고 한번더 중첩되었다 ==> 키 값을 넣을 수 있음
  // 키와 값 사이의 타입 관계를 보증하지 않는다
  private Map<Class<?>, Object> favorites = new HashMap<>();

  public <T> void putFavorites(Class<T> type, T instance) {
    favorites.put(Objects.requireNonNull(type), instance);
  }

  public <T> T getFavorites(Class<T> type) {
    // Class 객체가 가리키는 타입으로 동적 형변환
    return type.cast(favorites.get(type));    // cast 메서드의 시그니처가 Class 제네릭이다
  }
}

// **참고
public class Class<T> {
  T cast(Object obj);
}

위의 Favorites 클래스에는 두가지 제약이 있다.

  • 악의적인 클라이언트가 Class 객체를 raw 타입으로 넘기면 타입 안전성이 쉽게 깨진다
    • :arrow_right: Class.cast() 함수로 해결
  • 실체화 불가 타입에는 사용할 수 없다 : List<String>.class 넘기면 오류
    • :arrow_right: 슈퍼 타입 토큰으로 해결할 수 있다
    • 스프링 프레임워크에서는 아예 ParameterizedTypeReference 라는 클래스로 구현해 놓았다

코드 33-4 동적 형변환으로 런타입 타입 안정성 확보

1
2
3
4
//
public <T> void putFavorites(Class<T> type, T instance) {
  favorites.put(Objects.requireNonNull(type), type.cast(instance));
}

코드 33-5 asSubclass 를 사용해 한정적 타입 토큰을 안전하게 형변환 한다

1
2
3
4
5
6
7
8
9
10
static Annotation getAnnotation(AnnotatedElement element, String annotationTypeName) {
  Class<?> annotationType = null;   // 비한정적 타입 토큰
  try{
    annotationType = Class.forName(annotationTypeName);
  } catch( Exception ex){
    throw new IllegalArgumentException(ex);
  }
  // asSubclass 가 실패하면 ClassCastException 을 던진다
  return element.getAnnotation( annotationType.asSubclass(Annotation.class));
}

:bangbang:   핵심 정리

  • 타입 안전 이종 컨테이너는 Class 를 키로 쓰며, 이런 식으로 쓰이는 Class 객체를 타입 토큰이라 한다.
  • 또한 직접 구현한 키 타입도 쓸 수 있다.
    • 데이터베이스의 행(컨테이너)을 표현한 DatabaseRow 타입에는 제네릭 타입인 Column<T>를 키로 사용할 수 있다.

 
 

끝!   읽어주셔서 감사합니다.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.