제네릭 타입을 만들기 전에, 제네릭이 없는 Stack 코드를 살펴보자.
import java.util.Arrays;
import java.util.EmptyStackException;
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;
}
private void ensureCapacity() {
if (elements.length == size) {
elements = Arrays.copyOf(elements, 22*size+1);
}
}
}
제네릭 미사용의 문제: 타입 안전성을 보장하지 않는 Stack 클래스
pop 메서드는 스택에서 객체를 꺼낼 때 Object 타입으로 반환한다.
이는 클라이언트가 해당 객체를 실제로 사용할 수 있도록 적절한 타입으로 형변환해야 함을 의미한다.
예를 들어, String 타입의 객체를 푸시한 후 꺼내려고 할 때 String으로 형변환해야 한다.
예를 들어, 스택에 Integer를 푸시한 후 String으로 형변환하려 하면 다음과 같은 오류가 발생한다.
제네릭 타입의 Stack 으로 만들기
위 Stack 클래스는 원래 제네릭 타입이어야 한다. 그러니 제네릭으로 만들어보자.
제네릭 크래스로 만들기 위해서 다음과 같이 해야 한다.
- 클래스 선언에 타입 매개변수를 추가한다. (보통 E를 많이 사용한다.)
- Objec를 적절한 타입 매개변수로 바꾼다.
import java.util.EmptyStackException;
import java.util.Arrays;
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;
}
private void ensureCapacity() {
if (elements.length == size) {
elements = Arrays.copyOf(elements, 22*size+1);
}
}
}
이렇게 코드를 수정했을 때, 컴파일 오류가 발생한다.
제네릭 배열 생성 시 발생하는 컴파일 오류와 타입 소거의 한계
제네릭 타입 E를 사용하여 배열을 직접 생성하려고 할 때 발생한다.
Java에서는 제네릭 타입의 배열을 직접 생성할 수 없다. 이 제한은 Java의 타입 소거(type erasure) 메커니즘 때문에 발생한다.
Java의 제네릭은 런타임에 타입 정보가 지워지는 타입 소거(type erasure)를 사용한다.
실체화 불가 타입으로는 배열을 만들 수 없다.
즉, 제네릭 타입 정보는 컴파일 타임에만 유효하고 런타임에는 삭제된다.
따라서, 제네릭 타입 E는 런타임에 실체화되지 않는다. 이는 배열 생성 시에 E의 타입 정보를 알 수 없다는 것을 의미하며, 이로 인해 배열을 생성할 수 없다.
실체화 불가 타입으로는 배열을 만들 수 없으니 다음 방법으로 해결해보자.
첫 번째: Object 배열을 생성한 다음 제네릭 배열로 형변환해본다.
제네릭 배열 생성을 금지하는 제약을 대놓고 우회하는 방법이다.
- elements는 private 피드로 저장된다.
- E[]로 선언되어 push(E)만 담아 타입 캐스팅이 안전하다.
- @SuppressedWarning("unchecked")로 경고 메시지를 숨긴다.
import java.util.EmptyStackException;
import java.util.Arrays;
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;
}
private void ensureCapacity() {
if (elements.length == size) {
elements = Arrays.copyOf(elements, 2 * size + 1);
}
}
}
Stack을 사용한 예제 코드로도 확인해보자.
제네릭을 사용하여 배열 생성 시 발생하는 형변환 경고를 억제하고,
push와 pop 메서드를 통해 항상 타입 E로 유지하여 런타임에 형변환 문제를 방지했다.
장점
- 가독성이 더 좋다. (E 타입만을 받는다는 점을 어필한다.)
- 형변환을 배열 생성시 단한번만 해주면 된다.
단점
- 배열의 런타임 타입이 컴파일타임 타입과 달라 힙오염(heap pollution)이 일어날 수 있다.
heap pollution: 매개변수화 타입이 매개변수화 타입이 아닌 것을 참조할 떄 발생하는 현상.
두 번째: elements 필드의 타입을 E[]에서 Object[]로 바꾼다.
이렇게 바꾸면 첫 번째와는 다른 오류가 발생한다.
import java.util.EmptyStackException;
import java.util.Arrays;
public class Stack<E> {
private Object[] elements;
private int size = 0;
private static final int DEFAULT_INITIAL_CAPACITY = 16;
@SuppressWarnings("unchecked")
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();
}
E result = elements[--size]; // 에러 메시지 발생!
elements[size] = null;
return result;
}
private void ensureCapacity() {
if (elements.length == size) {
elements = Arrays.copyOf(elements, 2 * size + 1);
}
}
}
배열이 반환한 원소를 E로 형변환하면 아래와 같이 경고가 뜬다.
E가 실체화 불가 타입이므로 컴파일러는 런타임에 이뤄지는 형변환이 안전한지 증명할 방법이 없다.
이후 비검사 형변환을 수행하는 할당문에서만 경고를 숨긴다.
특징
- 배열에서 원소를 읽을 떄마다 형변환 해주어야 한다.
- 힙오염(heap pollution)이 해가 되지 않는다.
heap pollution: 매개변수화 타입이 매개변수화 타입이 아닌 것을 참조할 떄 발생하는 현상.
한정적 타입 매개변수 (bounded type parameter)
class DelayQueue<E extends Delayed> implements BlockingQueue<E>
Delayed.class의 하위타입만 받는다는 의미이다. (모든 타입은 자기 자신의 하위 타입)
ClassCastException을 걱정할 필요가 없다.
결론
클라이언트에서 직접 형변환해야 하는 타입보다 제네릭 타입이 더 안전하고 쓰기 편하다.
그렇기 떄문에 새로운 타입을 설계할 때는 형변환 없이 사용할 수 있도록 하라.
제네릭 타입을 사용하는 것은 클라이언트에는 아무 영향을 주지 않으면서, 새로운 사용자들을 편하게 해주는 방법이다.
'Dev Books > Effective Java' 카테고리의 다른 글
[item 31] 한정적 와일드카드를 사용해 API 유연성을 높여라 (0) | 2025.02.05 |
---|---|
[item 30] 이왕이면 제네릭 메서드로 만들라. (0) | 2025.02.04 |
[item 25] 톱레벨 클래스는 한 파일에 하나만 담으라 (0) | 2024.06.17 |
[item 28] 배열보다는 리스트를 사용하라 (0) | 2024.05.31 |
[item 27] 비 검사 경고를 제거하라 (0) | 2024.05.31 |
댓글