이왕이면 제네릭 타입으로 만들라
제네릭 타입을 새로 만드는 일은 조금 더 어렵다. 그래도 배워두면 그만한 값어치는 충분히 한다.
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;
}
poublic 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);
}
}
위는 제네릭이 절실하다. 제네릭으로 만들어보자.
일반 클래스를 제네릭 클래스로 만드는 첫 단계는 클래스 선언에 타입 매개변수를 추가하는 일이다. 코드 29-1의 예에서는 스택이 담을 원소의 타입 하나만 추가하면 된다. 이때 타입 이름으로는 보통 E를 사용한다.
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;
}
... // 나머지는 그대로다.
}
이 단계에서 대체로 하나 이상의 오류나 경고가 뜨는데, 이 클래스도 예외는 아니다. 여기서는 다행히 오류가 하나만 발생했다.
elements = new E[DEFAULT_INITIAL_CAPACITY];
배열을 사용하는 코드를 제네릭으로 만들려 할 때는 이 문제가 항상 발목을 잡을 것이다. 적절한 해결책은 두 가지다.
1. 제네릭 배열 생성을 금지하는 제약을 대놓고 우회하는 방법
Object 배열을 생성한 다음 제네릭 배열로 형변환한다. (E[]) new Object[DEFAULT_INITIAL_CAPACITY];
일반적으로 타입 안전하지 않다. 컴파일러는 이 프로그램이 타입 안전한지 증명할 방법이 없지만, 우리는 할 수 있다. 따라서 스스로 확인해야 한다. 배열 elements는 Private 필드에 저장되고, 클라이언트로 반환되거나 다른 메서드에 전달되는 일이 전혀 없다. push 메서드를 통해 배열에 저장되는 원소의 타입은 항상 E다. 따라서 확실히 안전하다.
비검사 형변환이 안전함을 증명했다면, 범위를 최소로 좁혀 @SupressWarnings 으로 경고를 숨긴다.
2. elements 필드의 타입을 E[]에서 Object[]로 바꾸는 것이다. 이렇게하면 첫번째와 다른 오류가 발생한다.
E result = elements[--size];
반환한 원소를 E로 형변환하면 오류 대신 경고가 뜬다.
E result = (E) elements[--size];
E는 실체화 불가 타입이므로 컴파일러는 런타임에 이뤄지는 형변환이 안전한지 증명할 방법이 없다. 이번에도 마찬가지로 우리가 직접 증명하고 경고를 숨길 수 있다.
첫 번째 방법은 가독성이 더 좋다. 코드도 더 짧다. 보통의 제네릭 클래스라면 코드 이곳저곳에서 이 배열을 자주 사용할 것이다. 첫 번째 방식에서는 형변환을 배열 생성 시 단 한 번만 해주면 되지만, 두 번째 방식에서는 배열에서 원소를 읽을 때마다 해줘야 한다. 따라서 현업에서는 첫 번째 방식을 더 선호하며 자주 사용한다.
지금까지 설명한 Stackk 예는 "배열보다는 리스트를 우선하라"는 아이템 28과 모순돼 보인다. 자바가 리스트를 기본 타입으로 제공하지 않으므로 어쩔 수 없는 부분이 있다.
클라이언트에서 직접 형변환해야 하는 타입보다 제네릭 타입이 더 안전하고 쓰기 편하다. 그러니 새로운 타입을 설계할 때는 형변환 없이도 사용할 수 있도록 하라. 그렇게 하려면 제네릭 타입으로 만들어야 할 경우가 많다. 기존 타입 중 제네릭이었어야 하는 게 있다면 제네릭 타입으로 변경하자.
'개발 > 이펙티브 자바' 카테고리의 다른 글
Effective Java ( 이펙티브 자바 ) - 아이템 31 (0) | 2021.06.24 |
---|---|
Effective Java ( 이펙티브 자바 ) - 아이템 30 (0) | 2021.06.24 |
Effective Java ( 이펙티브 자바 ) - 아이템 28 (0) | 2021.06.24 |
Effective Java ( 이펙티브 자바 ) - 아이템 27 (0) | 2021.06.17 |
Effective Java ( 이펙티브 자바 ) - 5장 제네릭 아이템 26 (0) | 2021.06.16 |