ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Item 29: Favor generic types
    독서/Effective Java 2022. 1. 2. 17:19

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

     

    이번 아이템에서는 아이템 7에서 다뤘던 Stack Code를 Generic Type으로 변경한다.

    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; // Eliminate obsolete reference
        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; // Eliminate obsolete reference
        return result;
      }
    
      public boolean isEmpty() {
        return size == 0;
      }
    
      private void ensureCapacity() {
        if (elements.length == size)
          elements = Arrays.copyOf(elements, 2 * size + 1);
      }
    }

    타입 매개변수 E를 사용하도록 변경했지만, 현재 상태에서는 컴파일이 불가능하다.

    Item 28에서 설명한 것처럼, E와 같은 실체화 불가 타입으로는 배열을 생성할 수 없다. 해결책은 두 가지이다.

     

    실체화 불가 타입으로 배열을 생성하는 방법

    첫 번째는 제네릭 배열 생성을 금지하는 제약을 대놓고 우회하는 방법이다. Object 배열을 생성한 다음 제네릭 배열로 형변환해보자. 이제 컴파일러는 오류 대신 경고를 내보낼 것이다. 이렇게도 할 수는 있지만 (일반적으로) 타입 안전하지 않다.

    컴파일러는 이 프로그램이 타입 안전한지 증명할 방법이 없지만 우리는 할 수 있다. 따라서 이 비검사 형변환이 프로그램의 타입 안정성을 해치지 않음을 우리 스스로 확인해야 한다. 문제의 배열 elements는 private 필드에 저장되고, 클라이언트로 반환되거나 다른 메서드에 전달되는 일이 전혀 없다. push 메서드를 통해 배열에 저장되는 원소의 타입은 항상 E다. 따라서 이 비검사 형변환은 확실히 안전하다.

    비검사 형변환이 안전함을 직접 증명했다면 범위를 최소로 좁혀 @SuppressWarnings 애너테이션으로 해당 경고를 숨긴다. 이 예에서는 생성자가 비검사 배열 생성 말고는 하는 일이 없으니 생성자 전체에서 경고를 숨겨도 좋다. 애너테이션을 달면 Stack은 깔끔히 컴파일되고, 명시적으로 형변환하지 않아도 ClassCastException 걱정 없이 사용할 수 있게 된다.

    // 배열 elements는 push(E)로 넘어온 E 인스턴스만 담는다. 
    @SuppressWarnings("unchecked")
    public Stack() {
      // 해당 배열의 런타임 타입은 E[] X -> Object[] O
      elements = (E[]) new Object[DEFAULT_INITIAL_CAPACITY];
    }

    두번째 방법은 elements 필드의 타입을 E[] -> Object[]으로 바꾸는 것이다. 바꾸고 보면 다음과 같은 에러가 발생된다.

    캐스팅으로 인해 에러 -> 경고로 변경됐다.

    E는 실체화 불가 타입이기 때문에 컴파일러는 런타임에 이뤄지는 형변환이 안전한 지 증명할 방법이 없다. 이번에도 위에서처럼 증명 후 경고를 숨겨보자. pop 메서드 전체를 @SuppressWarnings 어노테이션으로 감싸지말고, 해당 경고가 뜨는 부분에만 적용시키면 다음과 같다.

    public E pop() {
      if (size == 0)
        throw new EmptyStackException();
      @SuppressWarnings("unchecked") 
      E result = (E) elements[--size];
      elements[size] = null;
      return result;
    }

     

    두 방식의 장단점

    제네릭 배열 생성을 제거하는 두 방법 모두 나름의 지지를 얻고 있다. 첫 번째 방법은 가독성이 더 좋다. 배열의 타입을 E[]로 선언하여 오직 E 타입 인스턴스만 받음을 확실히 어필한다. 코드도 더 짧다. 보통의 제네릭 클래스라면 코드 이곳저곳에서 이 배열을 자주 사용할 것이다. 

    첫 번째 방식에서는 형변환을 배열 생성 시 단 한번만 해주면 되지만, 두 번째 방식에서는 배열에서 원소를 읽을 때마다 해줘야 한다. 따라서 현업에서는 첫 번째 방식을 더 선호하며 자주 사용한다. 하지만 (E가 Object가 아닌 한) 배열의 런타임 타입이 컴파일 타임 타입과 달라 힙 오염(heap pollution)을 일으킨다. 힙 오염이 맘에 걸리는 프로그래머는 두 번째 방식을 고수하기도 한다.

    사용 예

    @Test
    void exampleTest() {
      // Little program to exercise our generic Stack
      List<String> list = new ArrayList<>();
      list.add("a");
      list.add("b");
      list.add("c");
      Stack<String> stack = new Stack<>();
      for (String arg : list)
        stack.push(arg);
      while (!stack.isEmpty())
        System.out.println(stack.pop().toUpperCase());
    }

     

    댓글

Designed by Tistory.