먼저 이전에 작성했던 Stack을 확인해보자
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;
}
private void ensureCapacity() {
if (size >= elements.length) {
throw new ArrayIndexOutOfBoundsException();
}
}
public Object pop() {
if (size == 0) {
throw new EmptyStackException();
}
Object result = elements[--size];
elements[size] = null;
return result;
}
public boolean isEmpty() {
return size == 0;
}
}
지금 이 Stack은 위험이 내재되어있다. 런타임에 ClassCastException등의 오류가 발생 할 수 있다. 제네릭 타입으로 바꿔보자.
public class Stack<E> {
private E[] elements;
private int size = 0;
private static final int DEFAULT_INITIAL_CAPACITY = 16;
public Stack() { // 비검사 경고 발생. unchecked cast
elements = (E[]) new Object[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);
}
public static void main(String[] args) {
Stack<String> stack = new Stack<>();
for (String arg : args)
stack.push(arg);
while (!stack.isEmpty())
System.out.println(stack.pop().toUpperCase());
}
}
위 Stack에서 봐야할 점은 2가지다.
일반적으로 다운캐스팅은 ClassCastException
이 발생한다. String s = (String) new Object();
와 같이 다운캐스팅하면 말이다. 하지만 생성자에 사용된 다운캐스팅은 예외가 발생하지는 않는다. 이에 대해 말하면, Object배열을 아직 모르는 타입의 배열로 다운캐스팅하 꼴이다. 그러니, unchecked cast
라는 비검사 경고를 보여주는것이다.
하지만 우리는 이 비검사 경고에 대해 타입 안전함을 보장할 수 있다. push메소드에서 항상 E타입의 원소를 저장하고 있다. 따라서 배열에 저장될 원소들의 타입은 E 타입임이 확실하므로 @SuppressWarnings("unchecked")
로 경고를 숨겨도 된다.
힙 오염은 item32에서 다루고있는 내용이다. 먼저 간단히 말해서 힙 오염은, a variable of parameterized type points to an object that is not of that parameterized type. 즉, 매개변수화 타입의 변수가 타입이 다른 객체를 참조하면 힙 오염이 발생한다.
위 elements 배열은 런타임에는 Object 배열이다. 하지만 컴파일타임에는 E[]배열이었다. 그러니, 런타임에 E[] 배열이 Object배열을 참조하는 힙 오염
이 발생한다. 책에서는 위 코드에는 힙 오염이 해가 되지 않았다고 한다.
- 우리가 만든 Stack에서 발생한 힙 오염의 원인은
배열의 런타임 타입이 컴파일타임과 다른 Object라는 것이다.
- Object타입이라 함은, 모든 타입을 수용한다.
- 그럼 String타입 Stack에, Integer타입을 넣는 행위를 유도할 수 있다.
- 이러한 행위는 컴파일타임에는 통과되고, 런타임에 문제가 생길 것이다.
코드로 바꾸면!
Stack<String> stringSt = new Stack<>();
Stack<Integer> intSt = (Stack<Integer>) (Object) stringSt; // Stack 으로부터 발생된 힙 오염!
intSt.push(1);
intSt.push(2);
String pop = stringSt.pop(); //ClassCastException
System.out.println("pop = " + pop);
intSt과 stringSt는 동일한 Stack이다. intSt.push(1);
로 String타입 Stack에 Integer를 넣을 수 있다. 그 이유는, 런타임에는 Object[]배열이라는 것이다. 제네릭은 컴파일타임에 타입을 지정하여 런타임시 발생할 수 있는 오류를 줄이는데에 목적이 있다. 그런데 우리가 Stack<Integer> intSt = (Stack<Integer>)(Object) stringSt
라는 코드로 컴파일러가 stringSt변수가 참조하는 Stack객체의 타입을 올바르게 지정하였음에도 불구하고, 런타임에 Integer가 들어가게끔 만들어버린것이다.
따라서 런타임에 ClassCastException
가 발생하는 해
를 만들었다!
class Stack<E> {
private Object[] elements; // case 2
private int size = 0;
private static final int DEFAULT_INITIAL_CAPACITY = 16;
public Stack() {
elements = new Object[DEFAULT_INITIAL_CAPACITY]; // case 2
}
public void push(E e) {
ensureCapacity();
elements[size++] = e;
}
private void ensureCapacity() {
if (size >= elements.length) {
throw new ArrayIndexOutOfBoundsException();
}
}
public E pop() {
if (size == 0) {
throw new EmptyStackException();
}
@SuppressWarnings("unchecked") E result = (E) elements[--size]; // case 2
elements[size] = null;
return result;
}
public boolean isEmpty() {
return size == 0;
}
}
두 번째 방법은 내부에 사용되는 배열의 타입을 E[] -> Object[]
로 바꾸는 것이다. 이 방법의 단점은 원소 하나를 꺼낼 때마다 형변환을 해야한다는 점이다. 형변환은 DownCasting이라면 좀 더 느리다고는 한다. 하지만 형변환을 지양해야하는 이유는 유지보수성 측면에 있다고 한다.
effective-java스터디에서 공유하고 있는 전체 item에 대한 정리글