Object를 사용할 경우 클라이언트가 매번 형변환을 해야하고 그 과정에서 Runtime 에러가 발생하는 것을 막기 위해 이왕이면 제네릭타입으로 만들자.
■ Object기반 스택
이 클래스는 제네릭 타입이어야한다. 스택에서 꺼낸 객체를 형변환할 때 런타임 오류가 발생할 수 있다.
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);
}
}
■ 제네릭 스택으로 가는 첫 단계 - 컴파일되지 않는다.
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 메서드는 그대로다.
}
위의 코드에서는 하나의 오류가 발생한다. E와같은 실체화 불가 타입으로는 배열을 만들 수 없다.
Stack.java:8: generic array creation
elements = new E[DEFAULT_INITIAL_CAPACITY];
이를 우회할 수 있는 방법으로 첫번째가 Object 배열을 생성한 후 제네릭 배열로 형변환을 하는 것 이다. 이제 컴파일러는 오류 대신 경고를 내보낸다. 이 방식은 일반적으로 타입 안전하지 않다. 컴파일러는 이 타입이 안전한지 증명할 수 없지만 우리는 할 수 있다. 따라서 비검사 형변환 프로그램의 타입 안전성을 해치지 않음을 우리가 확인해야한다. push 메서드를 통해 배열에 저장되는 원소의 타입은 항상 E다. 따라서 이 비검사 형변환은 안전하기 때문에 @SuppressWarings를 붙여서 경고를 숨긴다.
// 배열 elements는 push(E)로 넘어온 E 인스턴스만 담는다.
// 따라서 타입 안정성을 보장하지만,
// 이 배열의 런타임 타입은 E[]가 아닌 Object[]다!
@SuppressWarnings("unchecked")
public Stack() {
elements = (E[]) new Object[DEFAULT_INITIAL_CAPACITY];
}
두번째 방법으로는 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는 실체화 불가 타입이므로 컴파일러는 런타임에 이루어지는 형변환이 안전한지 증명할 수 없다. 이번에도 마찬가지로 pop 메서드 전체에서 경고를 숨기지 말고 형변환을 수행하는 할당문의 경고만 숨긴다.
// 비검사 경고를 적절히 숨긴다
public E pop() {
if (size == 0)
throw new EmptyStackException();
// push에서 E 타입만 허용하므로 이 형변환은 안전하다.
@SuppressWarnings("unchecked") E result = (E) elements[--size];
elements[size] = null; // 다 쓴 참조 해제
return result;
}
제네릭 배열 생성을 제거하는 두 방법 모두 나름의 지지를 얻고 있다. 첫 번째 방법은 가독성이 더 좋다. 배열의 타입을 E[]로 선언하여 오직 E 타입 인스턴스만 받음을 확실히 어필한다. 코드도 더 짧다. 보통의 제네릭 클래스라면 코드 이곳저곳에서 이 배열을 자주 사용할 것이다. 첫 번째 방식에서는 형변환을 배열 생성 시 단 한번만 해주면 되지만, 두 번째 방식에서는 배열에서 원소를 읽을 때마다 해줘야 한다. 따라서 현업에서는 첫 번째 방식을 더 선호하며 자주 사용한다. 하지만 (E가 Object가 아닌 한) 배열의 런타임 타입이 컴파일타임 타입과 달라 힙 오염(heap pollution)을 일으킨다. 힙 오염이 맘에 걸리는 프로그래머는 두 번째 방식을 고수하기도 한다.
다음은 명령줄 인수들을 역순으로 바꿔 대문자로 출력하는 프로그램으로, 방금 만든 제네릭 Stack 클래스를 사용하는 모습을 보여준다. Stack에서 꺼낸 원소에서 String의 toUpperCase 메서드를 호출할 때 명시적 형변환을 수행하지 않으면, 이 형변환이 항상 성공함을 보장한다.
```java
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 예는 "배열보다는 리스트를 우선하라"는 아이템 28과 모순돼 보인다. 사실 제네릭 타입 안에서 리스트를 사용하는 게 항상 가능하지도, 꼭 더 좋은 것도 아니다. 자바가 리스트를 기본 타입으로 제공하지 않으므로 ArrayList 같은 제네릭 타입도 결국은 기본 타입인 배열을 사용해 구현해야 한다. 또한 HashMap 같은 제네릭 타입은 성능을 높일 목적으로 배열을 사용하기도 한다.
'Effective Java' 카테고리의 다른 글
[Effective Java] 아이템31 한정적 와일드카드를 사용해 API 유연성을 높이라 (0) | 2021.05.30 |
---|---|
[Effective Java] 아이템30 이왕이면 제네릭 메서드로 만들라 (0) | 2021.05.29 |
[Effective Java] 아이템28 배열보다는 리스트를 사용하라 (0) | 2021.05.23 |
[Effective Java] 아이템27 비검사 경고를 제거하라 (0) | 2021.05.22 |
[Effective Java] 아이템26 로 타입은 사용하지 말라 (0) | 2021.03.08 |