Effective Java - 제네릭(3)

SeungHyuk Shin·2021년 10월 7일
0

Effective Java

목록 보기
16/26
post-thumbnail
post-custom-banner

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


public class ArrayStack {
    private Object[] elements;
    private int size;

    public ArrayStack() {
        this.elements = new Object[10];
    }

    public void push(Object obj) {
        elements[size++] = obj;
    }

    public Object pop() {
        if (size == 0) {
            throw new IllegalArgumentException("스택에 아무것도 없습니다");
        }
        Object result = elements[--size];
        elements[size] = null;
        return result;
    }

}

이 클래스는 원래 제네릭 타입이어야 마땅하다. 클라이언트는 스택에서 꺼낸후 객체를 형변환해야 하는데 이때 런타임 오류가 날 확률이 있다. 일반 클래스를 제네릭 클래스로 만드는 첫 단계는 클래스 선언에 타입 매개변수를 추가하는 일이다. 이때 스택이 담을 원소를 추가하면 되므로 보통 E를 사용한다.

/* 컴파일 되지 않는다. */
public class ArraysStack<E> {

    public class ArrayStack {
        private E[] elements;
        private int size;

        public ArrayStack() {
            this.elements = new E[10];
        }

        public void push(E obj) {
            elements[size++] = obj;
        }

        public E pop() {
            if (size == 0) {
                throw new IllegalArgumentException("스택에 아무것도 없습니다");
            }
            E result = elements[--size];
            elements[size] = null;
            return result;
        }

    }
}

앞서보았듯이 제네릭은 불공변이므로 런타임시에 사라지고 E는 소거되기 때문인데, 컴파일 에러를 해결할 수 있는 방법은 2가지가 있다.

  1. 대놓고 우회하는 방법 생성자의 컴파일 에러를 다음으로 해결한다.
// 배열 elements는 push(E)로 넘어온 E 인스턴스만 담는다. 
// 따라서 타입 안정성을 보장하지만, 
// 이 배열의 런타임 타입은 E[]가 아닌 Object[]다! 
@SuppressWarnings("unchecked")
 public GenericStack() {
     this.elements = (E[])new Object[10];
 }

이 예제의 경우 E타입만 Push할 수 있기 때문에 자체적으로 형변환이 늘 안전하다는 것을 확신할 수 있다.
따라서 이렇게 우회하고 @SuppressWarnings 으로 비검사 오류를 숨길 수 있다.

장점: 필드에 E 배열 타입을 가지고 있음으로써 이 Stack은 E 타입의 인스턴스만 받을 수 있음을 확신할 수 있도록 한다.
단점: 힙 오염. 런타임타입: E, 컴파일타임 타입: Object

힙오염(https://en.wikipedia.org/wiki/Heap_pollution)
아래와 같이 컴파일 타입, 런타임 타입이 달라 Unchecked Warning 과 ClassCastException 이 발생할 가능성이 있는 상황.
스택 예의 경우 그럴 가능성은 없음

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

이렇게 하면 첫 번째와는 다른 오류가 발생한다.

Stack.java:19: incompatible types
found: Object, required: E
        E result = elements[--size];

배열이 반환한 원소를 E로 형변환하면 오류 대신 경고가 뜬다.

Stack.java:19: warning: [unchecked] unchecked cast
found: Object, required: E
        E result = (E) elements[--size];

E는 실체화 불가 타입이므로 컴파일러는 런타임에 이뤄지는 형변환이 안전한지 증명할 방법이 없다. 이번에도 마찬가지로 우리가 직접 증명하고 경고를 숨길 수 있다. pop 메서드 전체에서 경고를 숨기지 말고, 아이템 27의 조언을 따라 비검사 형변환을 수행하는 할당문에서만 숨겨보자.

// 비검사 경고를 적절히 숨긴다
public E pop() {
    if (size == 0)
        throw new EmptyStackException();

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

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

제네릭 배열 생성을 제거하는 두 방법 모두 나름의 지지를 얻고 있다. 첫 번째 방법은 가독성이 더 좋다. 배열의 타입을 E[]로 선언하여 오직 E 타입 인스턴스만 받음을 확실히 어필한다.

코드도 더 짧다. 보통의 제네릭 클래스라면 코드 이곳저곳에서 이 배열을 자주 사용할 것이다.

첫 번째 방식에서는 형변환을 배열 생성 시 단 한번만 해주면 되지만, 두 번째 방식에서는 배열에서 원소를 읽을 때마다 해줘야 한다.

따라서 현업에서는 첫 번째 방식을 더 선호하며 자주 사용한다. 하지만 (E가 Object가 아닌 한) 배열의 런타임 타입이 컴파일타임 타입과 달라 힙 오염(heap pollution)을 일으킨다.

힙 오염이 맘에 걸리는 프로그래머는 두 번째 방식을 고수하기도 한다.

다음은 명령줄 인수들을 역순으로 바꿔 대문자로 출력하는 프로그램으로, 방금 만든 제네릭 Stack 클래스를 사용하는 모습을 보여준다. Stack에서 꺼낸 원소에서 StringtoUpperCase 메서드를 호출할 때 명시적 형변환을 수행하지 않으면, 이 형변환이 항상 성공함을 보장한다.

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

지금까지 설명한 Stack 예는 "배열보다는 리스트를 우선하라"는 아이템 28과 모순돼 보인다. 사실 제네릭 타입 안에서 리스트를 사용하는 게 항상 가능하지도, 꼭 더 좋은 것도 아니다. 자바가 리스트를 기본 타입으로 제공하지 않으므로 ArrayList 같은 제네릭 타입도 결국은 기본 타입인 배열을 사용해 구현해야 한다. 또한 HashMap 같은 제네릭 타입은 성능을 높일 목적으로 배열을 사용하기도 한다.

Stack 예처럼 대다수의 제네릭 타입은 타입 매개변수에 아무런 제약을 두지 않는다. Stack<Object>, Stack<int[]>, Stack<List<String>>, Stack 등 어떤 참조 타입으로도 Stack을 만들 수 있다. 단, 기본 타입은 사용할 수 없다. Stack<int>Stack<double>을 만들려고 하면 컴파일 오류가 난다. 이는 자바 제네릭 타입 시스템의 근본적인 문제이나, 박싱된 기본 타입(아이템 61)을 사용해 우회할 수 있다.

타입 매개변수에 제약을 두는 제네릭 타입도 있다. 예컨대 java.util.concurrent.DelayQueue는 다음처럼 선언되어 있다.

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

타입 매개변수 목록인 <E extends Delayed>는 java.util.concurrent.Delayed의 하위 타입만 받는다는 뜻이다. 이렇게 하여 DelayQueue 자신과 DelayQueue를 사용하는 클라이언트는 DelayQueue의 원소에서 (형변환 없이) 곧바로 Delayed 클래스의 메서드를 호출할 수 있다. ClassCastException 걱정은 할 필요가 없다. 이러한 타입 매개변수 E를 한정적 타입 매개변수(bounded type parameter)라 한다. 그리고 하나 더! 모든 타입은 자기 자신의 하위 타입이므로 DelayQueue<Delayed> 로도 사용할 수 있음을 기억해두자.

post-custom-banner

0개의 댓글