Effective Java - Item 29

Updated:

Effective Java

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

제네릭 클래스로 만들기

  1. 타입 매개변수를 만들자.
    • 타입의 이름으로 보통 E를 사용한다.
  2. Object를 적절한 타입 매개변수로 변경한다.
public class Stack<E> {
  private E[] elements;   // Object를 E로, 두번째 방법은 Object[] elements로
  private int size = 0;
  private static final int DEFAULT_INITIAL_CAPACITY = 16;

  public Stack(){
    elements = new E[DEFAULT_INITIAL_CAPACITY]; // 첫번째 방법, (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];    // 두번째 방법,  E result = (E) 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와 같은 실체화 불가 타입으로는 배열을 만들 수 없다.
따라서, 두가지 방법으로 위를 해결한다.

첫번째 방법

Object 배열을 생성한 다음, 제네릭으로 형변환한다.

public Stack(){
  // 배열 elements 는 push(E)로 넘어온 E 인스턴스만 담는다.
  // 따라서 타입 안전성을 보장하지만,
  // 이 배열의 런타임 타입은 E[]가 아닌 Object[]다!
  @SuppressWarnings("unchecked")
  elements = (E[]) new Object [DEFAULT_INITIAL_CAPACITY];
}

장점

가독성 더 좋음
오직 E 인스턴스만 배열로 받음을 선언
코드 더 짧음
형번환을 배열 생성시 단 한번만 하면 됨

두번째 방법

elements 필드의 타입을 E[]에서 Object[]로 바꾼다.

public class Stack<E> {
  private Object[] elements;

  ~~~

  public E pop(){
    if(size == 0){
      throw new EmptyStackException();
    }

    @SuppressWarnings("unchecked")
    E result = (E) elements[--size]; // Object를 E로 형변환 하여 리턴
    elements[size] = null;
    return result;
  }
}

장점

힙 오염이 발생하지 않아 이를 더 선호하는 프로그래머들이 있음

타입 매개 변수의 한계

대다수의 제네릭 타입은 타입 매개변수에 아무런 제약을 두지 않는다.
ex) Stack, Stack<int[]>, Stack<List>, Stack 등.. 어떤 참조타입도 가능

단, 기본타입은 사용할 수 없다.
ex) Stack, Stack등.. 컴파일 오류 발생 이는 박싱된 기본타입으로 사용해 우회 가능하다.

확인

OpenJDK 1.8의 Stack을 직접 확인해 보았다.

public
class Stack<E> extends Vector<E> {
  public Stack() {
  }
  public E push(E item) {
      addElement(item);

      return item;
  }
  public synchronized E pop() {
      E       obj;
      int     len = size();

      obj = peek();
      removeElementAt(len - 1);

      return obj;
  } 
}

Stack은 Vector를 상속받아, 해당하는 메서드를 호출하면서 동작한다. 그렇다면, Vector의 구조를 확인하자.

public class Vector<E>
    extends AbstractList<E>
    implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
    protected Object[] elementData;   // item 29의 두번째 방법을 사용하였다.  

제네릭 클래스로 만들었을 때 생기는 제네릭 배열 생성 오류의 두번째 방법인 elements 필드 타입을 E[]에서 Object[]로 바꾼다를 사용하였다.
그럼 stack의 push와 pop을 실행할때 호출하는 vector 메서드 들을 보자,

// push
public synchronized void addElement(E obj) {
  modCount++;
  ensureCapacityHelper(elementCount + 1);
  elementData[elementCount++] = obj;
}
// pop
public synchronized E peek() {
  int     len = size();

  if (len == 0)
      throw new EmptyStackException();
  return elementAt(len - 1);
}

public synchronized E elementAt(int index) {
  if (index >= elementCount) {
      throw new ArrayIndexOutOfBoundsException(index + " >= " + elementCount);
  }

  return elementData(index);
}

@SuppressWarnings("unchecked")
E elementData(int index) {
    return (E) elementData[index];    // E로 직접 형변환 하였다.  
}

pop은 push에 비해 더 많은 메서드 들을 호출하는데, peek() -> elementAt() -> elementData() 순으로 호출한다.
그리고 elementData()에서 item28의 두번째 방법을 사용한 것을 확인 할 수 있다.

결론

직접 형변환 보다 제네릭 타입이 더 안전하고 쓰기 편하다.
기존 타입 중 제네릭이었어야 하는 것이 있다면 제네릭 타입으로 변경하자.

출처

Joshua Bloch. Effective Java 3/E. n.p.: 인사이트, 2018년 11월 1일.

Reference

https://sejoung.github.io/2018/12/2018-12-27-Prefer_lists_to_arrays/#%EC%95%84%EC%9D%B4%ED%85%9C-28-%EB%B0%B0%EC%97%B4%EB%B3%B4%EB%8B%A8-%EB%A6%AC%EC%8A%A4%ED%8A%B8%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%98%EB%9D%BC