반응형

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

 

이전 아이템에서 살펴보았듯이 타입에 대한 자유도 때문에 사용부에서 형변환을 많이 한다면 제네릭을 고려해야 한다.

Item 7 에서 다루었던 Stack 클래스를 제네릭 타입으로 변경해보면서 변환 시 주의할 점과 꿀팁들을 알아보자.

 

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

    private void ensureCapacity() {
        if (elements.length == size)
            elements = Arrays.copyOf(elements, 2 * size);
    }
}

 

이 원본 클래스에서는 클라이언트가 스택으로부터 객체를 꺼낼 때 Object 타입이기 때문에 매번 적절히 형변환을 해주어야 한다. 클라이언트에서 실수로 잘못 형변환할 경우 런타임 오류가 날 위험이 있다. 제네릭 타입으로 구현하는 것이 좋다.

 

먼저 클래스 선언에 타입 매개 변수를 추가해보자.

 

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

    private void ensureCapacity() {
        if (elements.length == size)
            elements = Arrays.copyOf(elements, 2 * size);
    }
}

 

위 코드에서 컴파일 에러가 나는 이유는 E 와 같은 실체화 불가 타입으로는 배열을 만들 수 없기 때문이다. 이에 대한 해결책으로 두 가지 방법이 있다.

 

첫 번째는 제네릭 배열 생성을 금지하는 제약을 우회하는 방법이다. Object 배열을 생성한 다음 제네릭 배열로 형변환한다. 컴파일러는 해당 형변환이 타입 안전한지 알 수 없기 때문에 비검사 형변환 경고를 띄운다. 하지만, elements 배열은 private 필드이고, 클라이언트로 반환되거나 다른 메서드에 전달되는 일이 없고, push() 메서드를 통해 배열에 저장되는 원소의 타입이 E 임을 알고 있다. 따라서 비검사 형변환은 확실하게 안전하므로 @SuppressWarnnings 애너테이션으로 해당 경고를 숨기는 것이 좋다.

 

public class Stack<E> {
    private E[] elements;
    private int size = 0;
    private static final int DEFAULT_INITIAL_CAPACITY = 16;

    // 배열 elements는 push(E)로 넘어온 E 인스턴스만 받는다.
    // 따라서 타입 안전성을 보장하지만,
    // 이 배열의 런타임 타입은 E[]가 아닌, 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;
        return result;
    }

    private void ensureCapacity() {
        if (elements.length == size)
            elements = Arrays.copyOf(elements, 2 * size);
    }
}

 

두 번째 방법은 elements 필드의 타입을 E[] 에서 Object[] 로 되돌리고, 에러가 나오는 구문에서만 형변환한다. pop() 메서드에서 타입 에러가 발생하는데, 해당 원소를 E 로 형변환 합니다. 형변환하면 아까와 마찬가지로 비검사 형변환에 대한 경고가 나타나는데, push() 에서 E 타입만 허용하므로 타입 안전성을 보장할 수 있으므로 @SuppressWarnning 애너테이션으로 경고를 숨겨줄 수 있다. 배열에 삽입하는 메서드들의 타입이 하나 뿐이어야만 가능하다는 점을 상기해야 한다.

 

import java.util.Arrays;
import java.util.EmptyStackException;

public class Stack<E> {
    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(E e) {
        ensureCapacity();
        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;
    }

    private void ensureCapacity() {
        if (elements.length == size)
            elements = Arrays.copyOf(elements, 2 * size);
    }
}

 

두 가지 방법 모두 잘 사용되고 있다. 첫 번째 방법은 배열의 타입을 오직 E 타입의 인스턴스로만 받을 수 있도록 명시하는 효과를 주며 배열 생성 시 한 번만 타입을 정하면 된다. 반면, 두 번째 방법은 배열에서 원소를 읽을 때마다 형변환을 해주어야 한다. 보통 첫 번째 방법을 더 선호한다. 하지만 배열의 런타임 타입과 컴파일타임 타입이 달라 힙 오염을 일으킬 수 있어서 두 번째 방식을 선택할 수 있다.

 

이전 아이템인 "배열보다는 리스트를 우선하라" 라는 이야기와 모순처럼 보이지만 결국 제네릭 타입도 내부적으로 배열을 사용하기 때문에 (성능 때문에 or 리스트가 기본 타입이 아니므로) 해당 방법을 알 필요가 있다.

반응형

+ Recent posts