제네릭 타입을 새로 만드는 일은 조금 어렵지만, 그만한 값어치는 충분히 한다.
아이템 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 = this.elements[--size];
this.elements[size]=null;
return result;
}
public void Example(){
Object age=13;
age=null;
}
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 = this.elements[--size];
this.elements[size]=null;
return result;
}
...
}
이 단계에서 대체로 하나 이상의 오류나 경고가 뜨는데, 위의 스택 클래스도 예외는 아니다.
오류의 원인은 아이템 28에서 설명한 것처럼, E
와 같은 실체화 불가 타입으로는 배열을 만들 수 없다. 배열을 사용하는 코드를 제네릭으로 만들려 할 때 항상 발생하는 문제이다. 해결 방법은 2가지이다.
elements=(E[])new Object[DEFAULT_INITIAL_CAPACITY];
Objcet
배열을 생성한 다음 제네릭 배열로 형변환 하는 것이다. 그러면 컴파일러는 오류 대신 비검사 형변환 경고를 내보낸다. 이렇게 할 수도 있지만 일반적으로 타입 안전하지 않다. 컴파일러는 이 프로그램의 타입이 안전한지 증명할 방법이 없기 때문에 우리가 스스로 비검사 형변환이 프로그램의 타입 안전성을 해치지 않음을 증명해야 한다. 예제 코드에서의 배열 elements
은 private
필드에 저장되고, 클라이언트로 반환되거나 다른 메서드에 전달되는 일이 없다. 그리고 push
메서드를 통해 배열에 저장되는 원소의 타입은 항상 E
다. 따라서 이 비검사 형변환은 확실히 안전하다.
비검사 형변환이 안전함을 직접 증명했다면 범위를 최소로 좁혀서 @SuppressWarnings
어노테이션으로 해당 경고를 숨긴다.
E result = elements[--size];
위의 코드에서 incompatible types
오류가 발생한다. 아래와 같이 수정하면 된다.
E result = (E) elements[--size];
그러면 비검사 형변환 경고가 뜬다. E
는 실체화 불가 타입이므로 컴파일러는 런타임에 이뤄지는 형변환이 안전한지 증명할 방법이 없다. 앞에서와 마찬가지로 우리가 직접 증명하고 경고를 숨기면 된다.
제네릭 배열 생성을 제거하는 두 방법 모두 괜찮은 방법이다. 첫 번째 방법은 가독성이 더 좋다. 배열의 타입을 E[]
로 선언하여 오직 E
타입 인스턴스만 받음을 확실히 어필한다. 코드도 더 짧다. 보통의 제네릭 클래스라면 코드 여러 곳에서 이 배열을 자주 사용할 것이다. 첫 번째 방식에서는 형변환을 배열 생성 시 단 한 번만 하면 되지만, 두 번째 방식은 배열에서 원소를 읽을 때마다 해줘야 한다. 따라서 현업에서는 첫 번쨰 방식을 더 선호하며 자주 사용한다. 하지만 배열의 런타임 타입이 컴파일타임 타입과 달라 힙 오염을 일으킨다. 힙 오염을 고려해서 두 번째 방식을 고수하는 프로그래머도 있다.
타입 매개변수에 제약을 두는 제네릭 타입도 있다. 예를 들면 java.util.concurrent.DelayQueue
는 다음과 같이 선언되어 있다.
class DelayQueue<E extends Delayed> implements BlockingQueue<E>
타입 매개변수 목록인 <E extends Delayed>
은 java.util.concurrent.Delayed
의 하위 타입만 받는다는 뜻이다. 이렇게 하여 DelayQueue
자신과 DelayQueue
를 사용하는 클라이언트는 DelayQueue
의 원소를 형변환 없이 곧바로 Delayed
클래스의 메서드를 호출할 수 있다. 이러한 타입 매개변수 E
를 한정적 타입 매개변수라 한다.