Item29. 이왕이면 제네릭 타입으로 만들라!
- 제네릭 타입을 새로 만드는 것은 조금 어려울 수 있지만, 알아두면 충분한 값어치를 할 것이다.
Object 기반 스택
- 제네릭이 절실한 강력 후보다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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
를 사용한다.
- 보통 타입의 이름으로
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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;
}
public boolean isEmpty() {
return size == 0;
}
private void ensureCapacity() {
if (elements.length == size) {
elements = Arrays.copyOf(elements, 2 * size + 1);
}
}
}
Object -> E
로 변경해준다.
변환 시 오류 발생
E
같은 실체화 불가 타입으로는 배열을 만들 수 없다.- 배열을 사용하는 코드를 제네릭으로 만들려 할 경우, 항상 이 문제가 생긴다.
오류 해결 방법 1 : 제네릭 배열 생성 금지 제약을 대놓고 우회하는 방법
Object 배열 생성 후 제네릭 배열로 형변환해보자.
1 2 3
public Stack() { elements = (E[]) new Object[DEFAULT_INITIAL_CAPACITY]; }
- 컴파일러는 오류 대신 경고를 내보낸다.
- 이러한 경우는 타입 안전하지 않다.
컴파일러로 타입 안전을 증명할 방법은 없다. 다만 우리는 할 수 있다. 직접 비검사 형변환 시 타입 안전성을 증명하자.
- 이 예제의 경우, elements 배열은 private 필드에 저장되고, 클라이언트에 반환되지 않는다. 또 push 메서드의 타입은 항상 E다.
- 해당 비검사 형변환은 안전함이 증명되었다.
- 안전함이 증명되었다면 @SuppressWarnings 애너테이션으로 해당 경고를 숨긴다.
- 애너테이션 통해 Stack은 깔끔히 컴파일되고, 명시적 형변환 없이도 ClassCastException 걱정을 덜 수 있다.
1 2 3 4
@SuppressWarnings("unchecked") public Stack() { elements = (E[]) new Object[DEFAULT_INITIAL_CAPACITY]; }
- 이 예제의 경우, elements 배열은 private 필드에 저장되고, 클라이언트에 반환되지 않는다. 또 push 메서드의 타입은 항상 E다.
오류 해결 방법 2 : elements 필드 타입을 E[] -> Object[]
이 경우, 첫번째 방법과는 다른 오류 발생
1
E result = elements[--size];
- 배열이 반환한 원소를 E로 형변환하면 오류 대신 경고가 뜬다.
- E는 실체화 불가 타입이다. 컴파일러는 런타임에 이뤄지는 형변환이 안전한지 증명할 수 없다.
- 이번에도 비검사 형변환을 수행하는 할당문에 @SuppressWarnings 애너테이션을 통해 경고를 숨길 수 있다.
- 배열이 반환한 원소를 E로 형변환하면 오류 대신 경고가 뜬다.
해결 방법의 장단점
- 첫 번째 방법의 장점
- 가독성이 좋다.
- 배열의 타입을 E[]로 선언하여 오직 E 타입 인스턴스만 받음을 확실히 어필한다.
- 코드가 짧다.
- 첫 번째 방식은 형변환을 배열 생성 시 한 번만 해주면 된다. 두 번째 방식은 배열에서 원소를 읽을 때마다 형변환한다.
- 현업에서는 첫 번째 방식을 더 선호한다.
- 가독성이 좋다.
- 첫 번째 방법의 단점
- E가 Object가 아닌 한 배열의 런타임 타입이 컴파일타임 타입과 달라 힙 오염을 일으킨다.
- 힙 오염이 걸리는 경우 두 번째 방식을 고수한다.
- E가 Object가 아닌 한 배열의 런타임 타입이 컴파일타임 타입과 달라 힙 오염을 일으킨다.
한정적 타입 매개변수
- 제네릭 타입 파라미터의 범위를 제한하는 방법이다.
1
class DelayQueue<E extends Delayed> implements BlockingQueue<E>
-
는 Delayed의 하위 타입만 받는다는 뜻이다. - 이렇게 하면 DelayQueue, 이를 사용하는 클라이언트는 DelayQueue의 원소에서 곧바로 Delayed 클래스의 메서드 호출이 가능해진다.
- 이 경우 ClassCastException은 걱정할 필요가 없다.
- 이렇게 하면 DelayQueue, 이를 사용하는 클라이언트는 DelayQueue의 원소에서 곧바로 Delayed 클래스의 메서드 호출이 가능해진다.
- 여기서 E를 한정적 타입 매개변수라 한다.
핵심 정리
- 클라이언트에서 직접 형변환해야 하는 타입보다 제네릭 타입이 더 안전하고 쓰기 편하다.
- 그러니 새로운 타입 설계 시, 형변환 없이도 사용 가능하게 하여라.
- 그러려면 제네릭 타입을 사용해야할 가능성이 크다.
- 그러니 새로운 타입 설계 시, 형변환 없이도 사용 가능하게 하여라.
- 기존 타입 중 제네릭이었어야 하는 게 있다면 제네릭 타입으로 변경하자.
- 기존 클라이언트에는 아무 영향도 주지 않는다.
- 동시에 새로운 사용자를 훨씬 편하게 해준다.
Comments powered by Disqus.