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

문법식·2022년 8월 16일
0

Effective Java 3/E

목록 보기
29/52

제네릭 타입을 새로 만드는 일은 조금 어렵지만, 그만한 값어치는 충분히 한다.
아이템 7에서 다룬 스택 코드를 예로 들겠다.

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 = this.elements[--size];
        this.elements[size]=null;

        return result;
    }

    public void Example(){
        Object age=13;
        age=null;
    }

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

이 스택 클래스를 사용하는 클라이언트는 스택에서 꺼낸 객체를 형변환해야 하는데, 이때 런타임 오류가 날 수 있다. 이 스택 클래스를 제네릭 타입으로 바꿔도 클라이언트에게 아무런 영향이 없고, 더 안전하다. 이 스택 클래스는 제네릭 타입이었어야 했다.

일반 클래스를 제네릭 클래스로 만드는 첫 단계는 클래스 선언에 타입 매개변수를 추가하는 것이다.

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 = this.elements[--size];
        this.elements[size]=null;

        return result;
    }
    ...
}

이 단계에서 대체로 하나 이상의 오류나 경고가 뜨는데, 위의 스택 클래스도 예외는 아니다.
오류의 원인은 아이템 28에서 설명한 것처럼, E와 같은 실체화 불가 타입으로는 배열을 만들 수 없다. 배열을 사용하는 코드를 제네릭으로 만들려 할 때 항상 발생하는 문제이다. 해결 방법은 2가지이다.

제네릭 배열 생성을 금지하는 제약을 우회하는 방법

elements=(E[])new Object[DEFAULT_INITIAL_CAPACITY];

Objcet 배열을 생성한 다음 제네릭 배열로 형변환 하는 것이다. 그러면 컴파일러는 오류 대신 비검사 형변환 경고를 내보낸다. 이렇게 할 수도 있지만 일반적으로 타입 안전하지 않다. 컴파일러는 이 프로그램의 타입이 안전한지 증명할 방법이 없기 때문에 우리가 스스로 비검사 형변환이 프로그램의 타입 안전성을 해치지 않음을 증명해야 한다. 예제 코드에서의 배열 elementsprivate 필드에 저장되고, 클라이언트로 반환되거나 다른 메서드에 전달되는 일이 없다. 그리고 push 메서드를 통해 배열에 저장되는 원소의 타입은 항상 E다. 따라서 이 비검사 형변환은 확실히 안전하다.
비검사 형변환이 안전함을 직접 증명했다면 범위를 최소로 좁혀서 @SuppressWarnings 어노테이션으로 해당 경고를 숨긴다.

elements 필드의 타입을 E[]에서 Object[]로 바꾸는 것

E result = elements[--size];

위의 코드에서 incompatible types 오류가 발생한다. 아래와 같이 수정하면 된다.

E result = (E) elements[--size];

그러면 비검사 형변환 경고가 뜬다. E는 실체화 불가 타입이므로 컴파일러는 런타임에 이뤄지는 형변환이 안전한지 증명할 방법이 없다. 앞에서와 마찬가지로 우리가 직접 증명하고 경고를 숨기면 된다.

제네릭 배열 생성을 제거하는 두 방법 모두 괜찮은 방법이다. 첫 번째 방법은 가독성이 더 좋다. 배열의 타입을 E[]로 선언하여 오직 E 타입 인스턴스만 받음을 확실히 어필한다. 코드도 더 짧다. 보통의 제네릭 클래스라면 코드 여러 곳에서 이 배열을 자주 사용할 것이다. 첫 번째 방식에서는 형변환을 배열 생성 시 단 한 번만 하면 되지만, 두 번째 방식은 배열에서 원소를 읽을 때마다 해줘야 한다. 따라서 현업에서는 첫 번쨰 방식을 더 선호하며 자주 사용한다. 하지만 배열의 런타임 타입이 컴파일타임 타입과 달라 힙 오염을 일으킨다. 힙 오염을 고려해서 두 번째 방식을 고수하는 프로그래머도 있다.

타입 매개변수에 제약을 두는 제네릭 타입도 있다. 예를 들면 java.util.concurrent.DelayQueue는 다음과 같이 선언되어 있다.

class DelayQueue<E extends Delayed> implements BlockingQueue<E>

타입 매개변수 목록인 <E extends Delayed>java.util.concurrent.Delayed의 하위 타입만 받는다는 뜻이다. 이렇게 하여 DelayQueue 자신과 DelayQueue를 사용하는 클라이언트는 DelayQueue의 원소를 형변환 없이 곧바로 Delayed 클래스의 메서드를 호출할 수 있다. 이러한 타입 매개변수 E한정적 타입 매개변수라 한다.

profile
백엔드

0개의 댓글