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 boolean isEmpty() {
return size == 0;
}
private void ensureCapacity() {
if (elements.length == size) {
elements = Arrays.copyOf(elements, 2 * size + 1);
}
}
}
이 클래스는 원래 제네릭 타입이어야 마땅하다.
- 지금 상태에서의 클라이언트는 스택에서 꺼낸 객체를 형변환해야 하는데, 이때 런타임 오류가 날 위험이 있다.
- 제네릭 클래스로 만드는 첫 단계는 클래스 선언에 타입 매개변수를 추가하는 일이다.
타입 이름으로는 보통 E를 사용한다. (아이템68)
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;
}
... // isEmpty와 ensureCapacity 메서드는 그대로다.
}
여기서 오류가 하나 발생한다.
Stack.java:8: generic array creation
elements = new E[DEFAULT_INITIAL_CAPACITY];
^
E와 같은 실체화 불가 타입으로는 배열을 만들 수 없다. (아이템28)
배열을 사용하는 코드를 제네릭으로 만들려 할 때는 이 문제가 항상 발목을 잡을 것이다.
해결책 1
제네릭 배열 생성을 금지하는 제약을 대놓고 우회
현업에서 더 자주 쓰는 방식
elements = (E[]) new Object[DEFAULT_INITIAL_CAPACITY];
- Object 배열을 생성한 다음 제네릭 배열로 형변환한다.
- 컴파일러는 오류 대신 경고를 내보낸다. 일반적으로 타입 안전하지 않다.
- 문제의 배열 elements는 private 필드에 저장되고, 클라이언트로 반환되거나 다른 메서드에 전달되는 일이 전혀 없다.
또한 push 메서드를 통해 배열에 저장되는 원소의 타입은 항상 E다.
따라서 이 비검사 형변환은 확실히 안전하다. - 비검사 형변환이 안전함을 증명했다면 범위를 최소로 좁혀 @SuppressWarnings 애너테이션으로 해당 경고를 숨긴다. (아이템27)
해결책 2
elements 필드의 타입을 E[]에서 Object[]로 바꾼다.
Stack.java:19: incompatible types
found: Object, required: E
E result = elements[--size];
^
배열이 반환한 원소를 E로 형변환하면 오류 대신 경고가 뜬다.
Stack.java:19: warning: [unchecked] unchecked cast
found: Object, required: E
E result = (E) elements[--size];
^
E는 실체화 불가 타입이므로 컴파일러는 런타임에 이뤄지는 형변환이 안전한지 증명할 방법이 없다.
비검사 형변환을 수행하는 할당문에서 경고를 숨겨보자.
// 비검사 경고를 적절히 숨긴다
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[]로 선언하여 오직 E 타입 인스턴스만 받음을 확실히 어필한다.
- 형변환을 배열 생성 시 단 한 번만 해주면 된다.
해결책 1의 단점
- E가 Object가 아닌 한 배열의 런타임 타입이 컴파일타임 타입과 달라 힙 오염(heap pollution; 아이템32)을 일으킨다.
해결책 2의 장점
- 힙 오염 걱정이 없다.
'독서찰기(讀書札記) > 이펙티브 자바' 카테고리의 다른 글
[아이템 31] 한정적 와일드카드를 사용해 API 유연성을 높이라 (0) | 2022.02.13 |
---|---|
[아이템 30] 이왕이면 제네릭 메서드로 만들라 (0) | 2022.02.12 |
[아이템 28] 배열보다는 리스트를 사용하라 (0) | 2022.02.07 |
[아이템 27] 비검사 경고를 제거하라 (0) | 2022.02.07 |
[아이템 26] 로 타입(raw type)은 사용하지 말라 (0) | 2022.02.06 |