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

제네릭 타입을 사용하는 것은 쉽지만 새로 만드는 것은 조금 더 어렵다. 하지만 배울 가치가 있기 때문에 이번 아이템에서 다룬다.
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() throws Exception{
if (size == 0)
throw new Exception();
Object result = elements[--size];
elements[size] = null; // 다 쓴 참조 해제
return result;
}
private void ensureCapacity() {
if (elements.length == size)
elements = Arrays.copyOf(elements, 2 * size + 1);
}
public boolean isEmpty() {
return size == 0;
}
}
위의 스택 클래스는 제네릭이어야 마땅하다. 자바5 이전에 작성된 코드라 하더라도 제네릭으로 바꾸어도 문제가 없다. 오히려 기존에 존재하는 형 변환 예외 가능성을 없애준다.
일반 클래스를 제네릭 클래스로 만들기
우선 클래스 선언에 타입 매개변수를 추가한다.
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() throws Exception{
if (size == 0)
throw new Exception();
E result = elements[--size];
elements[size] = null; // 다 쓴 참조 해제
return result;
}
...
}
이렇게만 할 경우 다음 라인에서 오류가 나며 컴파일 되지 않는다.
elements = new E[DEFAULT_INITIAL_CAPACITY];
실체화 불가 타입 E를 배열로 만드려고 했기 때문이다. 배열을 사용하는 코드를 제네릭으로 변경할 때 마다 항상 이 문제가 따라올 것이다.
해결 방법으로는 두가지가 있다.
우선 제약을 우회하는 방법이다. Object 배열을 만든 후 E[] 처럼 제네릭 배열로 형 변환하는 경우이다.
이렇게 바꾸면 컴파일러는 비검사 형변환 경고를 보낸다. elements는 private 필드이고 다른 메서드에서 반환되지 않는다. 또한 push를 통해 배열에 저장되는 원소의 타입은 E 이므로 이 형변환은 안전하다. 안전을 확인 했으니 애너테이션을 달아 경고를 숨기면 된다. 이 예에서는 생성자에서 비검사 배열 생성만 하니 생성자에 애너테이션을 달면 된다.
@SuppressWarnings("unchecked")
public Stack() {
elements = (E[]) new Object[][DEFAULT_INITIAL_CAPACITY];
}
다른 방법으로는 elements를 선언하는 부분에서 타입을 E[]가 아닌 Object로 바꾸는 것이다.
이렇게 하면 pop 메서드의
E result = elements[--size]; 부분에서 형식이 맞지 않아 오류가 나게 된다. 이 부분은 배열이 반환한 원소를 E 로 형변환 해주면 된다. 그래도 비검사 형변환 경고가 뜨지만 안전한지 증명 후 경고를 숨기면 된다. 이 경우는 형변환 하는 할당문에만 에너테이션을 달아주자
public class Stack<E> {
private Object[] elements;
...
public E pop() throws Exception{
if (size == 0)
throw new Exception();
@SuppressWarnings("unchecked") E result = (E) elements[--size];
elements[size] = null; // 다 쓴 참조 해제
return result;
}
...
}
위 두 방식의 장단점
- elements 초기화에 Object[]를 사용하는 방법
배열 타입을 E[]로 선언하여 E 타입 인스턴스만 받는다는 것을 어필하고 가독성도 좋다. 형변환을 한번만 해도 된다. 현업에서 선호되는 방식이다. 하지만 E가 Object가 아니면 배열의 런타임 타입이 컴파일타임 타입과 달라 힙 오염을 일으킨다. - elements 를 Object[]로 선언하는 방법
코드가 더 길고 가독성도 안좋다. 배열에서 원소를 읽을 때 마다 형 변환을 해줘야 한다.
하지만 E가 Object가 아니여도 힙 오염이 되지 않는다. (컴파일 자체가 안되기 때문인가..?)
지금까지 설명한 Stack은 리스트를 사용하라는 아이템 28과 모순돼 보이지만 제네릭 타입 안에 항상 리스트를 사용가능 한 것도 아니고 더 좋은 것도 아니다. ArrayList 같은 제네릭도 결국 배열을 사용해 구현해야 하고 HashMap같이 성능에 민감한 제네릭은 성능을 위해 배열을 사용하기도 한다.
대다수의 제네릭 타입은 타입 매개변수에 제약을 두지 않지만 원시타입은 불가능해 박싱된 기본타입을 사용해야 한다.
<E extends Delayed> 처럼 제약을 걸어 사용하기도 하는데 이때 E를 한정적 타입 매개변수라 한다.