Item 29 - 이왕이면 제네릭 타입으로 만들라
아이템 7에서 다룬 스택 코드
이 클래스는 원래 제네릭 타입이어야 마땅하다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 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;
}
//.. 생략
}
- 클라이언트는 스택에서 꺼낸 객체를 형변환해야 하는데, 이때 런타임 오류가 날 위험이 있다.
일반 클래스를 제네릭 클래스로 만드는 방법
- 클래스 선언에 타입 매개변수를 추가한다.
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
// 제네릭 스택으로 가는 첫 단계 - 컴파일 되지 않는다.
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]; // Error
}
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;
}
// .. 생략
}
- 위 코드는 컴파일 오류가 발생한다. 이는 아이템 28에서도 말한 내용이다.
E
와 같은 실체화 불가 타입으로는 배열을 만들 수 없다.
해결 방안 (비검사 형변환)
(1) Object 배열을 생성한 다음 제네릭 배열로 형변환한다.
1
2
3
4
@SuppressWarnings("unchecked")
public Stack() {
elements = (E[]) new Object[DEFAULT_INITIAL_CAPACITY];
}
- 컴파일러는 오류 대신 경고를 내보낸다.
- 실행은 할 수 있지만 타입 안전하지 않아서, 런타임 오류가 발생할 수도 있다.
- 따라서 이 비검사 형변환이 프로그램의 타입 안정성을 해치지 않음을 스스로 확인해야 한다.
- 배열 elements는 private 필드에 저장되고, 클라이언트로 반환되거나 다른 메서드에 전달될 일이 없다.
- push 메서드를 통해 배열에 저장되는 원소의 타입은 항상
E
다. - 따라서 이 비검사 형변환은 확실히 안전하다.
- 비검사 형변환이 안전함을 직접 증명했다면 범위를 최소로 좁혀
@SuppressWarnings
애너테이션으로 경고를 숨기자. - 이 방법은 가독성이 좋고 형변환을 배열 생성 시 단 한번만 하면 된다.
(2) elements 필드의 타입을 E[]에서 Object[]로 바꾼다.
1
2
3
4
5
6
7
8
9
10
11
12
13
// 배열을 사용한 코드를 제네릭으로 만드는 방법 2
// 비검사 경고를 적절히 숨긴다
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는 실체화 불가 타입이므로)
- push에서 E 타입만 허용하므로 이 형변환은 안전하다는 것을 알 수 있다. - 직접 증명
- 그래서 최소 범위인 할당문에서만 애너테이션을 붙여주어 경고를 숨겨주었다.
- 이 방법은 배열에서 원소를 읽을 때마다 형변환을 해줘야하는 불편함이 있다.
의문 ❓
- “배열보다는 리스트를 우선하라”는 아이템 28과 모순인 것 같다.
- 제네릭 타입 안에서 리스트를 사용하는게 항상 가능한 것도 아니고 꼭 더 좋은 것도 아니다.
- 자바가 리스트를 기본 타입으로 제공하지 않기 때문에,
ArrayList
같은 제네릭 타입도 결국에는 기본 타입인 배열을 사용해서 구현해야 한다.
제네릭 타입
- 타입 매개변수에 아무 제약도 두지 않는다.
- 단, 기본 타입은 사용 할 수 없다 → 박싱된 기본 타입(아이템 61)을 사용해 우회할 수 있다.
- 타입 매개변수에 제약을 두는 제네릭 타입도 있다.
- ex.
java.util.concurrent.DelayQueue
class DelayQueue<E extends Delayed> implements BlockingQueue<E>
java.util.concurrent.Delayed
의 하위 타입만 받는다는 뜻이다.DelayQueue
원소에서 형변환 없이 곧바로Delayed
클래스의 메서드를 호출할 수 있다.ClassCastException
걱정은 할 필요가 없다.
- ex.
- 모든 타입은 자기 자신의 하위 타입이므로
DelayQueue<Delayed>
로도 사용가능하다.
💡 핵심 정리
- 클라이언트에서 직접 형변환해야 하는 타입보다 제네릭 타입이 안전하고 편하다.
- 기존 타입 중에 제네릭이어야 하는게 있다면, 제네릭 타입으로 변경하자.
- 기존 클라이언트에는 아무 영향도 주지 않으면서 사용자를 편하게 한다.
참고 🕶️
한정적 타입 매개변수(Bounded Type Parameters)
- Bounded Type Parameters (The Java™ Tutorials > Learning the Java Language > Generics (Updated))
- 매개변수화 타입을 특정한 타입으로 한정 짓고 싶을 때, 사용할 수 있다.
<E extends Number>
: 선언할 수 있는 제네릭 타입을Number
를 상속(extnds)했거나 구현(implements)한 클래스로 제한한다.- 제한한 타입의 인스턴스를 만들거나, 메서드를 호출할 수도 있다.
<E extedns Number>
:Number
타입이 제공하는 메서드를 사용할 수 있다.- 다수의 타입으로 한정 할 수 있다. 이 때 클래스 타입을 가장 먼저 선언해야 한다.
<E extedns Numebr & Serializable>
: 선언할 제네릭 타입은Integer
와Number
를 모두 상속 또는 구현한 타입이어야 한다.
This post is licensed under CC BY 4.0 by the author.