Effective Java - Chapter 5

Updated:

Effective Java

[Effective Java 3/E] 5장 제네릭

Item 26. 로 타입은 사용하지 말라

제네릭 타입

클래스와 인터페이스 선언에 타입 매개변수가 쓰이는 것
제네릭을 활용하면 컬렉션 타입에 해당 인스턴스만 넣어야 함을 컴파일러가 인지하게 된다.

로 타입

제네릭 타입에서 타입 매개변수를 전혀 사용하지 않은 것.Set
로 타입을 사용하면 런타임에 예외가 일어날 수 있으니 사용해서는 안된다.
=> 그럼에도 사용하는 이유는 제네릭이 도입되기 이전 코드와의 호완성을 위해 제공한다.

매개변수화 타입

어떤 타입의 객체도 저장할 수 있다.Set<Object>

비 한정적 와일드카드 타입

모종의 타입 객체만 저장할 수 있다. Set<?>
Set<?>, Set<Object>은 안전하지만, 로타입 Set 은 안전하지 않다.

Item 27. 비검사 경고를 제거하라

모든 비검사 경고는 중요하다.
비검사 경고는 런타임에 ClassCastException을 일으킬수 있는 잠재적 가능성을 뜻하니, 할 수 있는 한 모든 비검사 경고를 제거하라.
모두 제거한다면 그 코드는 타입 안정성이 보장된다.
경고를 제거할 수는 없지만 타입 안전하다고 확신 한다면 @SuppressWarings("unchecked") 에너테이션을 달아 경고를 숨긴다.
SuppressWarings 에너테이션은 항상 가능한 좁은 범위에 적용한다.
그런다음 경고를 숨기기로 한 근거를 주석으로 남겨라.

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

배열과 제네릭 타입의 차이

  • 배열
    공변이고 실체화 된다.
    런타임에는 타입 안전하지만 컴파일 타임에는 그렇지 않다.
  • 제네릭
    불공변이고 런타임에 타입 정보가 소거된다.(실체화 불가 타입)
    컴파일 타임에는 타입 안전하지만 런타임에는 그렇지 않다.
    따라서 둘을 섞어 쓰는 제네릭 배열을 생성하지 못한다. new List<E>[]
    둘을 섞어 쓰다가 컴파일 오류나 경고를 만나면, 배열을 리스트로 대체하는 방법을 사용하자. E[] -> List<E>

Item 29. 이왕이면 제네릭 타입으로 만들라

직접 형변환 보다 제네릭 타입이 더 안전하고 쓰기 편하다.
기존 타입 중 제네릭이었어야 하는 것이 있다면 제네릭 타입으로 변경하자.

제네릭 클래스로 만들기

  1. 타입 매개변수를 만들자.
    • 타입의 이름으로 보통 E를 사용한다.
  2. Object를 적절한 타입 매개변수로 변경한다.
public class Stack<E> {
  private E[] elements;   // Object를 E로, 두번째 방법은 Object[] elements로
  private int size = 0;
  private static final int DEFAULT_INITIAL_CAPACITY = 16;
public Stack(){
    elements = new E[DEFAULT_INITIAL_CAPACITY]; // 첫번째 방법, (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];    // 두번째 방법,  E result = (E) 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);
    }
  }
}

컴파일 오류 발생!!!
E와 같은 실체화 불가 타입으로는 배열을 만들 수 없다.
따라서, 두가지 방법으로 위를 해결한다.

첫번째 방법

Object 배열을 생성한 다음, 제네릭으로 형변환한다.

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

장점

가독성 더 좋음
오직 E 인스턴스만 배열로 받음을 선언
코드 더 짧음
형번환을 배열 생성시 단 한번만 하면 됨

두번째 방법

elements 필드의 타입을 E[]에서 Object[]로 바꾼다.

public class Stack<E> {
  private Object[] elements;
  ~~~
  public E pop(){
    if(size == 0){
      throw new EmptyStackException();
    }
@SuppressWarnings("unchecked")
    E result = (E) elements[--size]; // Object를 E로 형변환 하여 리턴
    elements[size] = null;
    return result;
  }
}

장점

힙 오염이 발생하지 않아 이를 더 선호하는 프로그래머들이 있음

타입 매개 변수의 한계

대다수의 제네릭 타입은 타입 매개변수에 아무런 제약을 두지 않는다.
ex) Stack<Object>, Stack<int[]>, Stack<List<String>>, Stack 등.. 어떤 참조타입도 가능

단, 기본타입은 사용할 수 없다.
ex) Stack<int>, Stack<double>등.. 컴파일 오류 발생
이는 박싱된 기본타입으로 사용해 우회 가능하다.

Item30. 이왕이면 제네릭 메서드로 만들라

제네릭 타입과 마찬가지로, 클라이언트에서 입력 매개변수와 반환값을 명시적으로 형변환해야 하는 메서드보다 제네릭 메서드가 더 안전하며 사용하기도 쉽다.

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

재귀적 타입 한정(Recursive type bound)

자기 자신이 들어간 표현식을 사용하여 타입 매개변수의 허용 범위를 한정.
재귀적 타입 한정은 주로 타입의 자연적 순서를 정하는 Comparable 인터페이스와 함께 쓰인다.

public interface Comparable<T> {
    int compareTo(T o);
}

Comparable 인터페이스의 타입 매개변수 T는 해당 인터페이스를 구현한 타입이 비교할 수 있는 원소의 타입을 정의

public static <E extends Comparable<E>> E max(Collection<E> c) {
    if (c.isEmpty())
        throw new IllegalArgumentException("컬렉션이 비어 있습니다.");
    
    E result = null;
    for (E e : c)
        if (result == null || e.compareTo(result) > 0)
            result Object.requireNonNull(e);
    
    return result;
}

타입 한정인 <E extends Comparable<E>>는 “모든 타입 E는 자신과 비교할 수 있다”라고 해석

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

매개변수화 타입은 불공변이다.
따라서 List<Object>List<String>은 아무 관계도 아니다.
위와 같은 상,하위 타입을 제네릭에서 표현하는 방법이 한정적 와일드카드 타입이다.

PECS : producer-extends, consumer-super

매개변수화 타입 T가 생산자면 <? extends T>를 사용하고 소비자라면 <? super T>를 사용하라

public void pushAll(Iterable<? extends E> src) { // 생산자
    for (E e : src)
    push (e);
}

외부 타입의 데이터를 Stack의 내부 인스턴스인 Number[] 넘겨준다.   

public void popAll(Collection<? super E> dst) { // 소비자
  dst.addAll(list);
  list.clear();
}

Object 타입에 Stack의 Number 타입 데이터를 넘겨준다.
Comparable과 Comparator는 모두 소비자이다.

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

가변인수 메서드

메서드에 넘기는 인수의 개수를 클라이언트가 조절할 수 있게 해준다.
하지만, 가변인수 메서드를 호출하면 가변인수를 담기 위한 배열이 만들어진다.
이는 제네릭 배열이 생성되며 이는 힙 오염의 위험이 있다.

제네릭 varargs 매개변수는 타입 안전하지는 않지만, 허용된다.

메서드에 제네릭(혹은 매개변수화된) varargs 매개변수를 사용하고자 한다면, 먼저 그 메서드가 타입 안전한지 확인한 다음 @SafeVarags 애너테이션을 달아 사용하는데 불편함이 없게끔 하자.

  • varargs 매개변수 배열에 아무것도 저장하지 않는다.
  • 그 배열의 참조가 밖으로 노출되지 않는다.
    위 조건을 만족하여 순수하게 인수들을 전달하는 일만하다면 그 메서드는 안전하다.

Item33. 타입 안전 이종 컨테이너를 고려하라

  • 컬렉션 API로 대표되는 일반적인 제네릭 형태에서는 한 컨테이너가 다룰 수 있는 타입 매개변수의 수가 고정되어 있다.
    EX) Set, Map<K,V>

  • 하지만 컨테이너 자체가 아닌 키를 타입 매개변수로 바꾸면 이런 제약이 없는 타입 안전 이종 컨테이너를 만들 수 있다.

public class Favorites { // 타입 이종 컨테이너 구현
  private Map<Class<?>, Object> favorites = new HashMap<>();
  public <T> void putFavorite(Class<T> type, T instance) {
    favorites.put(Objects.requireNonNull(type), type.cast(instance));
  }
  public <T> T getFavorite(Class<T> type) {
    return type.cast(favorites.get(type));
  }
}
  • 타입 안전 이종 컨테이너는 Class를 키로 쓰며, 이런식으로 쓰이는 Class 객체를 타입 토큰이라 한다.
    EX) String.class, Integer.class

출처

Joshua Bloch. Effective Java 3/E. n.p.: 인사이트, 2018년 11월 1일.

Reference