✒️ 핵심 정리
클라이언트에서 직접 형변환해야 하는 타입보다 제네릭 타입이 더 안전하고 쓰기 편하다.
그러니 새로운 타입을 설계할 때는 형변환 없이도 사용할 수 있도록 하라. 그렇게 하려면 제네릭 타입으로 만들어야 하는 경우가 많다.
제네릭 타입을 사용하는 것은 클라이언트에는 아무 영향을 주지 않으면서, 새로운 사용자들을 편하게 해주는 방법이다. (Item 26)
제네릭 타입을 새로 만드는 일은 조금 더 어렵다.
그래도 배워두면 그만한 값어치는 충분히 한다.
📌 As-is
Item 7에서 다룬 단순한 스택 코드를 다시 살펴보자.
public class MyStack {
private Object[] elements;
private int size = 0;
private static final int DEFAULT_INITIAL_CAPACITY = 16;
public MyStack() {
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 boolean isEmpty() {
return size == 0;
}
private void ensureCapacity() {
if (elements.length == size) {
elements = Arrays.copyOf(elements, 2 * size + 1);
}
}
public static void main(String[] args) {
MyStack stack = new MyStack();
stack.push(1);
stack.push("1");
stack.pop().getClass();
stack.pop().getClass();
}
}
현재의 문제점 클라이언트가 스택에서 꺼낸 객체를 형변환해야 한다는 점이다.
개발자가 실수한 경우 해당 에러는 컴파일 타임이 아니라 런타임에서 발생하여 문제가 커진다.
📌 To-be
일반 클래스를 제네릭 클래스로 만드는 첫 번째 단계는 클래스 선언에 타입 매개변수를 추가하는 일이다.
스택이 담을 원소의 타입 하나만 추가하면 된다. 이때 타입 이름은 보통 E를 사용한다. (Item 68)
그런 다음 코드에 쓰인 Object를 적절한 타입 매개변수로 바꾸고 컴파일해보자.
public class MyStack<E> {
private E[] elements;
private int size = 0;
private static final int DEFAULT_INITIAL_CAPACITY = 16;
public MyStack() {
elements = new E[DEFAULT_INITIAL_CAPACITY]; // Error
}
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;
}
...
}
✒️ 문제점
• E와 같은 실체화 불가 타입으로는 배열을 만들 수 없다. (생성자에서 에러 발생)
• 자바에서는 제네릭 클래스를 인스턴스화 할 때 해당 타입을 소거하기 때문이다.
• 해당 타입은 컴파일 타임까지만 존재하고, 컴파일이 끝난 바이트 코드에서는 어떠한 타입 파라미터 정보도 찾을 수 없다.
1️⃣ 해결 방법 1. 제네릭 배열 생성을 금지하는 제약을 대놓고 우회
elements = (E[]) new Object[DEFAULT_INITIAL_CAPACITY];
- 컴파일러에서 오류 대신 경고를 내보내지만, 일반적으로 type-safe하지 않다.
- 컴파일러는 type-safe를 증명할 방법이 없지만 우리가 직접 확인해볼 수는 있다.
- 문제의 배열 elements는 private 필드에 저장되고, 클라이언트로 반환되거나 다른 메서드로 전달되지 않는다.
- push 메서드를 통해 배열에 저장되는 원소 타입은 언제나 E다. 따라서 비검사 형변환은 확실히 안전하다.
- 검증이 끝났으므로 생성자 전체에 @SuppressWarnings 어노테이션을 추가한다.
2️⃣ 해결 방법 2. elements 필드의 타입을 E[]에서 Object[]로 바꾸는 것
public class Stack<E> {
// private으로 저장
private Object[] elements;
...
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;
}
...
}
- 컴파일 에러는 발생하지 않지만, (1)과 다른 경고가 출력된다. E result = elements[--size];에서 경고
- E는 실체화 불가 타입이기 때문에 컴파일러는 런타임에 이루어지는 형변환 안전성을 증명할 수 없다.
- 마찬가지로 직접 증명하고 숨길 수 있다.
두 가지 방법 모두 나름의 지지도를 얻는다.
첫 번째 방법은 가독성이 좋다. 배열의 타입을 E[]로 선언하여 오직 E 타입 인스턴스만 받음을 확실히 한다.
형변환을 배열 생성 시 단 한 번만 해주는 것도 좋다.
그래서 현업에서는 전자를 선호하고 자주 사용하긴 하나, (E가 Object가 아닌 한) 배열의 런타임 타입이 컴파일 타임 타입과 달라 힙 오염(heap pollution, Item 32)을 일으킨다. (이번 예제에선 해당되지 않는다.)
📌 유의할 내용들
- "배열보다는 리스트를 우선하라"(Item 28)과 모순돼 보일 수 있다.
- 제네릭 타입 안에서 리스트를 사용하는 게 항상 가능하지도, 꼭 더 좋은 것도 아니다.
- ArrayList와 같은 제네릭 타입도 결국 기본 타입인 배열을 사용해 구현해야 한다.
- HashMap의 경우 성능을 높일 목적으로 배열을 사용하기도 한다.
- 대다수의 제네릭 타입은 타입 매개변수에 아무런 제약을 두지 않는다.
- Stack<Object>, Stack<int[]>, Stack<List<String>>, Stack 등 어떤 참조 타입으로도 Stack을 만들 수 있다.
- 단, 기본 타입은 사용할 수 없다. 박싱된 기본 타입으로 우회할 수는 있다. (제네릭의 근본적 문제)
- 타입 매개변수(bounded type parameter)를 사용해 매개변수에 제약을 둘 수도 있다.
- class DelayQueue<E extends Delayed> implements BlockingQueue<E>
- 타입 매개변수 목록인 <E extends Delayed>는 Delayed 하위 타입만을 받는다.
- DelayQueue의 원소에서 형변환 없이 바로 Delayed 메서드를 호출할 수도 있다.