[Effective Java] 이왕이면 제네릭 타입으로 만들라

들어가며

제네릭 타입과 메서드를 사용하는 일은 일반적으로 쉬운 편이 아니지만 제네릭 타입을 새로 만드는 일은 조금 더 어렵다. 그래도 이를 알아두면 값어치는 충분히 한다.

다음 코드들은 Stack을 제네릭 타입으로 바꾸는 두 가지 방법을 제시한다.

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
32
33
public class Stack<E> {
private E[] elements;
private int size = 0;
private static final int DEFAULT_INITIAL_CAPACITY = 16;

//우회한다
@SuppressWarnings("unchecked")
public Stack() {
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);
}
}
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
32
public class Stack<E> {
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(E e) {
ensureCapacity();
elements[size++] = e;
}

public E pop() {
if (size == 0)
throw new EmptyStackException();
@SuppressWarnings("unchecked")
E result = (E) 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가 Object가 아닌 한 배열의 런타임 타입이 컴파일 타입과 달라 힙 오염을 일으킨다.

두 번째 방법은 elements 필드의 타입을 E[]에서 Object[]로 바꾸는 것이다. 힙 오염을 예방할 수는 있지만 배열에서 원소를 읽을 때 마다 형 변환을 수행한다.

제네릭 타입 안에서 리스트를 사용하는게 항상 가능하지도, 꼭 더 좋은 예도 아니다. 자바가 리스트를 기본 타입으로 제공하지 않으므로 ArrayList 같은 제네릭 타입도 결국 기본 타입인 배열을 사용해 구현해야 한다. 또한 HashMap 같은 제네릭 타입은 성능을 위해 배열을 사용한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

/**
* Shared empty array instance used for empty instances.
*/
private static final Object[] EMPTY_ELEMENTDATA = {};

/**
* Shared empty array instance used for default sized empty instances. We
* distinguish this from EMPTY_ELEMENTDATA to know how much to inflate when
* first element is added.
*/
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

/**
* The array buffer into which the elements of the ArrayList are stored.
* The capacity of the ArrayList is the length of this array buffer. Any
* empty ArrayList with elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA
* will be expanded to DEFAULT_CAPACITY when the first element is added.
*/
transient Object[] elementData; // non-private to simplify nested class access

Stack 처럼 대다수의 제네릭 타입은 타입 매개변수에 아무런 제약을 두지 않는다. 단, 기본 타입은 제외하고 말이다. Stack<int> 이는 자바 제네릭의 근본적인 문제다.

타입 매개변수에 제약을 두는 제네릭 타입도 있다. class DelayQueue<E extends Delayed> implements BlockingQueue<E> 타입 매개변수 목록인 <E extends Delayed>java.util.concurrent.Delayed의 하위 타입만 받는다는 뜻이다. 이러한 타입 매개변수를 한정적 타입 매개변수라고 한다. 모든 타입은 자기 자신의 하위 타입이므로 DelayQueue로도 사용할 수 있음을 기억하자.

Author: Song Hayoung
Link: https://songhayoung.github.io/2020/08/11/Languages/Effective%20JAVA/item29/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.