Skip to content

item 29 hyowon

lsucret edited this page Aug 7, 2020 · 1 revision

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

아이템 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 = 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);
        }
    }
}

변경 후 - 컴파일되지 않는다.

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

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

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

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

Object를 E로 바꾸는 단계에서 보통 하나 이상의 오류나 경고가 뜬다.

여기서는 E와 같은 실체화 불가 타입으로 배열을 만들 수 없어 오류가 뜬다.

해결책1. 제네릭 배열 생성을 우회

// 오류 제거
public Stack(){
    elements = (E[]) new Object [DEFAULT_INITIAL_CAPACITY]}
}

이제 컴파일러는 오류 대신 경고를 내보낼 것이다.

런타임에 오류가 날 가능성이 있는데, 컴파일러는 가능성을 확인할 수 없지만 우리는 할 수 있다.

아래 배열 elements는 private 필드에 저장되고, 클라이언트로 반환되거나 다른 메서드에 전달되는 일이 없다.

또한 push 메서드로 저장되는 원소의 타입은 항상 E이다. 따라서 이 비검사 형변환은 안전하다.

// 경고 제거
@SuppressWarnings("unchecked")
public Stack(){
    elements = (E[]) new Object [DEFAULT_INITIAL_CAPACITY]}
}

안전함을 코드상으로 확인했으니 @SuppressWarnings 을 최소 범위로 붙여 경고를 숨긴다.(item27)

어노테이션을 달면 Stack이 깔끔하게 컴파일되고, 명시적으로 형변환하지 않아도 ClassCastException 걱정 없이 사용할 수 있게 된다.

해결책2. elements필드의 타입을 E[] → Object[]로 변경한다.

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();
            }
 
            E result = (E) elements[--size]; // <------------ <1>
            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> : Object 배열을 E 타입 result에 대입하려 해서 오류가 뜨니, 형변환해 대입한다.

그럼 경고가 뜨는데 마찬가지로 개발자가 직접 런타임 오류가 날 구석이 없는지 확인 후, 어노테이션을 붙인다.

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

    @SuppressWarnings("unchecked")
    E result = (E) elements[--size];

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

두 방법의 장단점

해결책1. 제네릭 배열 생성을 우회

  • 가독성이 좋다
  • (형변환을 배열 생성 시 한 번만 해주면 되니)코드가 더 짧아진다.
  • 배열의 런타임타입이 컨타일타임타입과 달라 힙 오염(item 32)을 일으킨다.
  • 힙 오염(heap pollution) : 파라미터 화 된 형태의 변수가 매개 변수 유형이 아닌 객체를 참조 할 때 발생하는 상황
public class HeapPollutionDemo {
   public static void main(String[] args) {
      Set s = new TreeSet<Integer>();
      Set<String> ss = s;            // unchecked warning
      s.add(new Integer(42));        // another unchecked warning
      Iterator<String> iter = ss.iterator();
      while (iter.hasNext()) {
         String str = iter.next();   // ClassCastException thrown
         System.out.println(str);
      }
   }
}

해결책2. elements필드의 타입을 E[] → Object[]로 변경

  • 1보다 코드가 더 길어진다.
  • 힙 오염이 없다.

기타

아이템 28의 "배열보다는 리스트를 우선하라" 는 말과 모순되는 예제이지만

자바의 기본 타입에 리스트가 제공되지 않아 결국엔 리스트를 배열로 구현해야 하며,

성능상 이점을 위해 배열을 일부러 사용하기도 한다.

제네릭 타입에 제약을 둘 수도 있다.

DelayQueue의 원소에서 형변환 없이 바로 Delayed 클래스의 메서드를 호출할 수 있고, ClassCastException 걱정을 할 필요도 없다.

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

모든 타입은 자기 자신의 하위 타입이므로 DelayQueue로도 사용할 수 있다.

결론

새로운 타입을 설계할 때는 형변환 없이도 사용할 수 있도록 하라.

그렇게 하려면 제네릭 타입으로 만들어야 할 경우가 많다.

Clone this wiki locally