[Object 기반 스택 - 제네릭이 절실한 강력후보]
public class ObjectStack {
private Object[] elements;
private int size = 0;
private static final int DEFAULT_INITIAL_CAPACITY = 16;
public ObjectStack() {
this.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
를 사용한다.
[제네릭 스택으로 변경]
public class GenericStack<E> {
private E[] elements;
private int size = 0;
private static final int DEFAULT_INITIAL_CAPACITY = 16;
public GenericStack() {
this.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;
}
public boolean isEmpty() {
return size == 0;
}
private void ensureCapacity() {
if (elements.length == size) {
elements = Arrays.copyOf(elements, 2 * size + 1);
}
}
}
E
와 같은 실체화 불가 타입으로는 배열을 만들 수 없기 때문이다.1. Object
배열을 생성하고 그 다음 제네릭 배열로 형변환하는 제네릭 배열 생성을 금지하는 제약을 대놓고 우회하는 방법.
[Object 배열 생성 시 배열 형변환]
...
public void push(E e) {
ensureCapacity();
elements[size++] = e;
}
public GenericStack() {
elements = (E[]) new Object[DEFAULT_INITIAL_CAPACITY]; // 경고 메시지 발생
}
private void ensureCapacity() {
if (elements.length == size) {
elements = Arrays.copyOf(elements, 2 * size + 1);
}
}
...
// 비검사 형변환 경고 메시지 발생
Unchecked cast: 'java.lang.Object[]' to 'E[]
elements
배열은 private
필드에 저장된 후 클라이언트로 반환되거나 다른 메서드에 전달되지 않는다.push
메서드를 통해 배열에 저장되는 원소의 타입은 항상 E
이다.@SuppressWarnings("unchecked")
를 사용하여 경고를 숨긴다.[비검사 형변환 경고 숨김]
...
public void push(E e) {
ensureCapacity();
elements[size++] = e;
}
// 배열 elements는 push(E)로 넘어온 E인스턴스만 남는다.
// 타입 안정성을 보장하지만 이 배열의 런타임 타입은 Object[] 이다.
@SuppressWarnings("unchecked")
public GenericStack() {
elements = (E[]) new Object[DEFAULT_INITIAL_CAPACITY]; // 경고 메시지 발생
}
private void ensureCapacity() {
if (elements.length == size) {
elements = Arrays.copyOf(elements, 2 * size + 1);
}
}
...
// 비검사 형변환 경고 메시지 발생
Unchecked cast: 'java.lang.Object[]' to 'E[]
2. elements
필드의 타입을 E[]
에서 Object[]
로 바꾸는 방법.
[Object 배열은 그대로 두고 pop() 사용시 형변환]
// 비검사 경고를 적절히 숨긴다.
public class GenericStack<E> {
private Object[] elements;
...
public GenericStack() {
this.elements = new Object[DEFAULT_INITIAL_CAPACITY];
}
public void push(E e) {
ensureCapacity();
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;
}
...
}
E
가 Object
가 아니기 때문에 (배열의 컴파일타임의 타입과 런타임의 타입이 다르기 때문에) 힙 오염을 일으킨다.실체화 불가 타입을 다루는 과정에서 컴파일러의 경고를 우회하는 모순적인 상황이 발생하지만 제네릭 타입이 주는 타입 안정성은 굉장한 편리함을 주기 때문에 제네릭을 적절히 그리고 적극적으로 사용하자.
[Reference]
힙 오염