아이템 29. 이왕이면 제네릭 타입으로 만들라. - ksw6169/effective-java GitHub Wiki

스택 클래스를 제네릭 타입으로 바꿔보자.

  • 다음 클래스를 그대로 쓰면 스택에서 꺼낸 객체를 형변환해야 하는데, 이때 런타임 오류가 날 위험이 있다.
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);
    }
}

일반 클래스를 제네릭 클래스로 만드는 방법

클래스 선언에 타입 매개변수를 추가한다.

  • 스택이 담을 원소의 타입 하나만 추가하면 된다.
  • 이때 타입 이름으로는 보통 E를 사용한다.
public class Stack<E> {
    private E[] elements;
    private int size = 0;
    private static final int DEFAULT_INITIAL_CAPACITY = 16;

    public Stack() {
        elements = new E[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;
    }
}
  • 위 클래스를 컴파일하면 오류가 발생한다.
  • E와 같은 실체화 불가 타입으로는 배열을 만들 수 없다는 뜻이다.
Stack.java:8: generic array creation
    elements = new E[DEFAULT_INITIAL_CAPACITY];
               ^
  • 이 문제의 적절한 해결책은 두 가지다.

배열을 사용하는 코드를 제네릭으로 만들려할 때의 해결책

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

public class Stack<E> {
    private E[] elements;

    ...

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

    ...
}
  • 컴파일러는 오류 대신 경고를 내보낼 것이다. (이 방법은 일반적으로 타입 안전하지 않다.)
Stack.java:8: warning: [unchecked] unchecked cast
found: Object[], required: E[]
    elements = (E[]) new Object[DEFAULT_INITIAL_CAPACITY];
                   ^
  • 예제 코드에서의 배열은 private 필드에 저장되고, 클라이언트에 반환되거나 다른 메소드에 전달되는 일이 전혀 없다. 또한 push 메소드를 통해 배열에 저장되는 원소의 타입은 항상 E이므로 이 비검사 형변환은 확실히 안전하다.
  • 비검사 형변환이 안전함을 직접 증명했으므로 범위를 최소로 좁혀 @SuppressWarnings 를 사용해 해당 경고를 숨기면 된다.
// 배열 elements는 push(E)로 넘어온 E 인스턴스만 담는다.
// 따라서 타입 안전성을 보장하지만,
// 이 배열의 런타임 타입은 E[]가 아닌 Object[]다!
@SuppressWarnings("unchecked")
public Stack() {
    elements = (E[]) new Object[DEFAULT_INITIAL_CAPACITY];
}

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

  • 이렇게 하면 첫 번째와는 다른 오류가 발생한다.
  • 배열이 반환한 원소를 E로 형변환하면 오류 대신 경고가 뜬다.
Stack.java:19: incompatible types
found: Object, required: E
    E result = elements[--size];
                       ^
  • E는 실체화 불가 타입이므로 컴파일러는 런타임에 이뤄지는 형변환이 안전한지 증명할 방법이 없다.
  • 이번에도 마찬가지로 타입 안전성을 증명하고 경고를 숨길 수 있다.
// 비검사 경고를 적절히 숨긴다.
public E pop() {
    if (size == 0)
        throw new EmptyStackException();

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

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

첫 번째 방법과 두 번째 방법의 차이점

[배열을 사용하는 코드를 제네릭으로 만들려할 때의 해결책]
(1) Object 배열을 생성한 다음 제네릭 배열로 형변환한다.
(2) elements 필드의 타입을 E[] 에서 Object[] 로 바꾼다.

[차이점]
- (1)이 가독성이 더 좋다. 배열의 타입을 E[]로 선언하여 E 타입의 인스턴스만 
  받음을 확실히 어필한다. 또한 코드도 더 짧다.

- (1)에서는 형변환을 배열 생성 시 단 한번만 해주면 되지만, (2)에서는 배열에서 
  원소를 읽을 때마다 해줘야 한다.

- 따라서 현업에서는 (1)의 방식을 더 선호하며 자주 사용한다.

- 하지만 (E가 Object가 아닌 한) 배열의 런타임 타입이 컴파일타임 타입과 달라 
  힙 오염(heap pollution)을 일으킨다. 힙 오염이 마음에 걸리는 프로그래머는 
  두 번째 방식을 고수하기도 한다.

제네릭 타입으로 만든 스택을 사용해보자.

  • 다음 코드는 Stack에서 꺼낸 원소에서 String의 toUpperCase를 호출할 때 명시적 형변환이 수행되지 않으며, 컴파일러에 의해 자동 생성된 형변환이 항상 성공함을 보장한다.
public 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());
}

참고 자료

  • Effective Java 3/E
⚠️ **GitHub.com Fallback** ⚠️