2 minute read

item 29 이왕이면 제너릭 타입으로 만들라.

다음은 제너릭을 사용하지 않은 Stack 클래스이다.

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 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;
    }
    ... // 타 메서드는 동일하다.
}

이를 해결하기 위해서는,

  1. Object 배열로 생성 후 제네릭으로 타입캐스팅 하거나
  2. elements 필드를 제네릭 배열E[]에서 Object[] 로 변경한다.

Object 배열을 제너릭 배열으로 타입캐스팅

비검사 형변환으로, Object 배열 생성 후 제너릭으로 형변환하는 방법이다.

@SuppressWarnings("unchecked")
public Stack() {
    elements = (E[]) new Object[DEFAULT_INITIAL_CAPACITY];
    //             ^ Unchecked cast 발생 : 
	// 컴파일러에서 타입 안정성이 보장되지 않는다는 경고가 발생한다.
}
  • elementsprivate 필드에 저장되며, 클라이언트로 반환되거나 다른 메서드로 전달되는 일이 없다.
  • push 메서드를 통해 배열에 저장되는 원소의 타입은 항상 E이다.

따라서 이 비검사 형변환이 type-safe함을 확인할 수 있으므로, @SuppressWarnings 어노테이션으로 경고를 숨긴다.

  • 위와 같은 방식에서는 형변환을 배열 생성시 최초 1회만 해주면 된다.
  • 하지만 런타임 타입과 컴파일타임 타입과 달라 *힙 오염이 발생할 수 있다.

*힙 오염 : JVM 메모리 공간인 Heap 영역에서, 매개변수화 타입 변수가 다른 타입의 객체를 참조해 발생한다.

필드 타입을 제네릭 배열E[]에서 객체 배열인 Object[] 로 변경

변경하게 되면, pop 메서드의 element 원소 접근 과정에서 오류가 발생하게 된다.

	E result = elements[--size];
					// ^ incompatible type
	// 제네릭 타입에 Object 타입이 할당되려 한다.

문제가 되는 부분은은 첫번째 방법에서의 해결법처럼, (여기에서는 메서드 내에서의) 타입캐스트와 @SuppressWarnings로 해결할 수 있다.

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 E pop() {
        if (size == 0)        
        throw new EmptyStackException();    
        // push에서 E 타입만 허용하므로 이 형변환은 안전하다.   
        @SuppressWarnings("unchecked") E result = (E) elements[--size];
        elements[size] = null; // 다 쓴 참조 해제    
        return result;
        }
	// etc ...
}

두번째 방식에서는 코드가 길어지고 배열에서 원소를 읽을 때마다 형변환을 해줘야 하는 단점이 있지만, 힙 오염은 일어나지 않는다.

한정적 타입 매개변수 (Bounded Type Parameter)

  • 타입 매개변수에 제약을 두는 제네릭 타입이다.

Upper bounded type

  • class UpperBoundedClass<E extends Example> 와 같이, 제네릭 타입에 extends 를 사용한다.
    • 특정 클래스(Example)의 자기 자신 및 하위 타입만 타입 변수로 받을 수 있다.
    • 클래스(UpperBoundedClass) 내에서 타입 매개변수(UpperBoundedClass)의 메서드를 호출할 수 있다.

Lower bounded type

  • class LowerBoundedClass<E super Example> 와 같이, 제네릭 타입에 super 를 사용한다.
    • 특정 클래스(Example)의 자기 자신 및 상위 타입만 타입 변수로 받을 수 있다.

Leave a comment