Post

Effective Java - item 29

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

Effective Java - item 29

들어가며

JDK가 제공하는 제네릭 타입과 메서드를 사용하는 일은 일반적으로 쉬운 편이지만 제네릭 타빙르 새로 만드는 일은 조금 더 어렵다. 그래도 배워두면 그만한 값어치는 충분히 한다.

제네릭 없는 스택

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
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);
    }
}

이 상태에서는 클라이언트가 스택에서 꺼낸 객체를 형변환해야 하는데 이때 런타임 오류가 날 위험이 있다. 일반 클래스를 제니릭 클래스로 만드는 첫 단계는 클래스 선언에 타입 매개 변수를 추가하는 것이다.

제네릭 스택

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
// Generic stack using E[] (Pages 130-3)
public class Stack<E> {
    private E[] elements;
    private int size = 0;
    private static final int DEFAULT_INITIAL_CAPACITY = 16;

    // The elements array will contain only E instances from push(E).
    // This is sufficient to ensure type safety, but the runtime
    // type of the array won't be E[]; it will always be Object[]!
    @SuppressWarnings("unchecked")
    public Stack() {
        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; // Eliminate obsolete reference
        return result;
    }
    ///...

이 단계에서는 E와 같은 실체화 불가 타입으로는 배열을 만들 수 없다. 배열을 사용하는 코드를 제네릭으로 만들려 할 때는 항상 이 문제가 발생한다. 어떤 방법을 선택하든, 비검사 형변환이 프로그램의 타입 안전성을 해치지 않음을 우리 스스로 확인해야 한다.

비검사 형변환이 안전함을 직접 증명했다면

  • 범위를 최소로 좁혀 @SuppressWarnings 애너테이션으로 해당 경고를 숨긴다
  • 애너테이션을 달면 Stack은 깔끔히 컴파일되고
  • 명시적으로 형변환하지 않아도 ClassCastException 걱정 없이 사용할 수 있다

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

Object 배열을 생성한 후 제네릭 배열로 형변환

1
2
3
4
@SuppressWarnings("unchecked")
public Stack() {
    elements = (E[]) new Object[DEFAULT_INITIAL_CAPACITY];
}

배열 elements는 push(E)로 넘어온 E 인스턴스만 담아서 타입 안전성을 보장한다. 하지만 이 배열의 런타임 타입은 E[]가 아닌 Object[]다 (힙 오염이 발생하기도 한다.)

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

elements 필드 타입을 Object[]로 바꾸기

1
2
3
4
5
6
public E pop() {
    // push에서 E 타입만 허용하므로 이 형변환은 안전하다.
    @SuppressWarnings("unchecked") 
    E result = (E) elements[--size];
    return result;
}

push(E)에서 E 타입만 허용하므로 배열에는 E 타입 인스턴스만 들어있기 떄문에 이 형변환은 안전하고 힙 오염이 발생하지 않는다.

타입 매개변수의 종류

  • 비한정적 타입 매개변수: Stack - 모든 타입을 받을 수 있음
  • 한정적 타입 매개변수: DelayQueue - 특정 타입의 하위 타입만 받을 수 있으며, 해당 타입의 메서드를 형변환 없이 사용 가능

사용예시

아래는 명령줄 인수들을 역순으로 바꿔 대문자로 출력하는 프로그램으로 책에서 제시한 사용 예시이다. Stack에서 꺼낸 원소에서 String의 toUpperCase 메서드를 호출할 때 명시적 형변환을 수행하지 않으며 컴파일러에 의해 자동 생성된 이 형변환이 항상 성공함을 보장한다.

1
2
3
4
5
6
7
8
// Little program to exercise our generic Stack
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());
}

마치며

클라이언트에서 직접 형변환해야 하는 타입보다 제네릭 타입이 더 안전하고 쓰기 편하다. 그러니 새로운 타입을 설계할 때는 형변환 없이도 사용할 수 있도록 하라. 그렇게 하려면 제네릭 타입으로 만들어야 할 때가 많다. 기존 타입 중 제네릭이었어야 하는 게 있다면 제네릭 타입으로 변경하자.

This post is licensed under CC BY 4.0 by the author.