이펙티브 자바 #item29 이왕이면 제네릭 타입으로 만들어라

임현규·2023년 2월 6일
0

이펙티브 자바

목록 보기
29/47

제네릭 타입이 형변환을 이용한 방법보다 좋은 이유

형변환은 잠재적으로 런타임 예외를 일으킬 가능성이 있다. item28에서 공변인 배열보다 리스트를 활용하라고 하는데 그런 예가 적절한 예라고 할 수 있다. 제네릭을 활용하면 타입에 대한 안정성을 높이고 컴파일 단계에서 걸러낼 수 있기 때문에 훨씬 안정적인 코드라 할 수 있다.

코드 개선하기

Object 기반 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;
    }

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

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

Object를 elements로 담은 Stack 클래스는 Object는 모든 클래스의 슈퍼 클래스이기 때문에 어떤 타입이든 담을수 있다는 것이 상당히 매력적으로 다가올 수 있다. 그러나 문제는 꺼내서 활용할 때인데 Object가 아닌 다른 값으로 형변환하면서 ClassCastException이 발생할 수 있다.

class SolutionTest {

    @Test
    void test() {
        Stack stack = new Stack();
        stack.push(1);
        stack.push("2");
        stack.push(50);
        while (!stack.isEmpty()) {
            int value = (int) stack.pop();
            System.out.println(value);
        }

    }
}

실제로 위와 같은 테스트 코드를 짜고 테스트를 수행하면

ClassCastException이 발생함을 알 수 있다. 원인은 당연히 정수형과 문자형을 혼합해서 Object[] 에 담고 있는데 정수형으로 형변환 도중 문자열 타입인 string과 정수형은 서로 관계가 없기 때문에 형변환을 실패하고 예외가 발생하는 것이다. 이런 경우 제네릭을 활용해 타입을 제한해서 활용하는 것이 코드를 더욱 안정적으로 활용할 수 있는 방법이다.

매개변수 타입 E를 활용해 제네릭으로 구현

public class Stack<E> {

    private E[] elements;
    private int size = 0;
    private static final int DEFAULT_INITIAL_CAPACITY = 16;

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

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

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

E를 활용해 제네릭으로 구현했다. 여기서 생성자 부분을 주목해 보면 (E[])로 Object[]를 형변환했음을 알 수 있다. 그 이유는 E는 실체화 불가능한 타입이기 때문이다.

그러나 위의 코드는 경고가 뜬다. 그 이유는 이 프로그램의 타입 안전에 대해 증명할 방법이 없기 때문이다. 해당 경고를 무시하는 방법은 @SupressWarnings("unchecked")를 붙여주는 방법이다. 그러면 경고 없는 제네릭 타입의 Stack을 만들 수 있다.

item28과는 모순된 리스트대신 배열 사용

item28에서는 안정적인 타입과 공변이 가지는 문제점 때문에 배열보다는 리스트를 우선하라고 했다. 왠만하면 리스트를 사용하는 것이 안전하고 좋지만 항상 좋은 것은 아니다. 예를 들면 ArrayList와 같은 제네릭 타입 클래스도 내부는 배열로 이루어지고 HashMap 같은 경우도 성능을 높일 목적으로 배열을 사용하기도 한다.

무조껀 배열이 안좋은 것은 아니다.. 하지만 배열을 제네릭으로 사용하고자 한다면 @SupressWarnings 활용은 물론 형변환시 발생할 수 있는 위험을 반드시 테스트해봐야 한다. 만약 상속을 활용한 클래스라면 상속시도 테스트해봐야 한다.

profile
엘 프사이 콩그루

0개의 댓글