클라이언트에서 직접 형변환해야 하는 타입보다 제네릭 타입이 더 안전하고 쓰기 편합니다. 이를 위해서는 제네릭 타입으로 만들어야 할 경우가 많습니다. 새로운 타입을 설계할 때 뿐만 아니라 기존 타입 중 제네릭이있어야 하는 게 있다면 제네릭 타입으로 변경하는 것이 좋습니다.
간단한 실습을 통해서 기존 코드를 제네릭 타입으로 바꾸는 것을 알아보겠습니다.
기존 코드
public class Stack {
private Object[] elements;
private int size = 0;
private static final int DEFAULT_INITIAL_CAPACITY = 16;
public Stack() {
this.elements = new Object[DEFAULT_INITIAL_CAPACITY];
}
public void push(Object e) {
ensureCapacity();
elements[size++] = e;
}
private void ensureCapacity() {
if (elements.length == size) {
elements = Arrays.copyOf(elements, 2 * size + 1);
}
}
public Object pop() {
if (size == 0) throw new EmptyStackException();
Object result = elements[--size];
elements[size] = null; // 다 쓴 참조 해제
return result;
}
public boolean isEmpty() {
return size == 0;
}
}
elements의 타입이 Object[]이기 때문에 push 메서드의 파라미터 타입도 Object이고 pop 메서드의 반환타입도 Object입니다. 따라서 이 클래스를 사용하면 값을 꺼낼 때 항상 형변환을 해야하고, 이 때 런타입 오류가 날 위험이 있습니다.
1. 클래스 선언에 타입 매개변수 추가
public class Stack<E> {
private E[] elements;
private int size = 0;
private static final int DEFAULT_INITIAL_CAPACITY = 16;
public Stack() {
this.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;
}
... // isEmpty와 ensureCapacity의 코드는 그대로
}
일반 클래스를 제네릭 클래스로 만드는 첫 단계는 클래스 선언에 타입배개 변수를 추가하는 일입니다. 이때는 보통 E를 사용합니다.
위 코드에서 Object였던 것을 전부 E로 바꿨습니다.
코드를 보면 이상한 부분이 있는데 역시 컴파일 오류가 발생합니다. 바로 생성자의 new E[DEFAULT_INITIAL_CAPACITY]
부분 입니다. E와 같은 실체화 불가 타입으로는 배열을 만들 수 없기 때문에 컴파일 오류가 발생하는 것 입니다.
이 때 적절한 해결책은 두가지가 있는데 첫 번째는 제네릭 배열 생성을 금지하는 제약을 우회하는 것이고 elements 필드의 타입을 E[]
에서 Object[]
로 바꾸는 것 입니다.
먼저 첫 번째 제네릭 배열 생성을 금지하는 제약을 우회하는 방법을 알아보겠습니다.
...
@SuppressWarnings("unchecked")
public Stack() {
this.elements = (E[]) new Object[DEFAULT_INITIAL_CAPACITY];
}
...
elements는 push(E)로 넘어온 E 인스턴스만 담으므로 타입 안전성을 보장합니다.
하지만 이 배열의 타입은 Object[]이기 때문에 Uncecked cast: 'java.lang.Object[]' to 'E[]'
란 경고가 발생합니다. 그래서 생성자에 @SuppressWarnings("unchecked")
을 붙여 해당 경고를 제거합니다.
다음으로 두 번째 방법인 elements의 타입 자체를 Object[]로 바꾸는 방법을 알아보겠습니다.
public class Stack<E> {
private Object[] elements;
private int size = 0;
private static final int DEFAULT_INITIAL_CAPACITY = 16;
public Stack() {
// new Object[]로 변경
this.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;
}
}
elements와 생성자 부분을 Object[]로 변경해주면 pop메서드에서 java: incompatible types: java.lang.Object cannot be converted to E
라는 컴파일 오류가 발생합니다. 이를 고치기 위해 배열의 반환 타입을 E로 형변환을 해줍니다.
...
public E pop() {
if (size == 0) throw new EmptyStackException();
E result = (E) elements[--size];
elements[size] = null;
return result;
}
...
이러면 첫 번째 방법과 동일하게 Unchecked cast: 'java.lang.Object' to 'E'
란 경고가 발생합니다. 마찬가지로 @SuppressWarnings("unchecked")
를 붙여 경고를 제거할 수 있습니다.
...
public E pop() {
if (size == 0) throw new EmptyStackException();
@SuppressWarnings("unchecked") E result = (E) elements[--size];
elements[size] = null;
return result;
}
...
두 방법 중 첫 번째 방법은 가독성이 더 좋습니다. 또한 첫 번째 방법은 형변환을 배열 생성시 해주면 되지만, 두 번째 방법은 pop을 할 때마다 형변환을 해야합니다. 따라서 현업에서는 첫 번째 방식을 더 선호하며 자주 사용합니다.
하지만 첫 번째 방법은 E가 Object인 경우를 제외하면 배열의 런타임 타입이 컴파일타임 타입과 달라 힣ㅂ 오염(heap pollution)을 일으킵니다. 힙 오염이 맘에 걸리면 두 번째 방법을 사용하는 것도 좋습니다.
elements를 배열이 아닌 리스트로 사용면 되는 것 아닌가 하는 생각이 들지만 제네릭 타입 안에서 리스트를 사용하는게 항상 가능하지도, 꼭 더 좋은 것도 아닙니다. 리스트는 기본 타입으로 제공되지 않으므로 ArrayList 같은 제네릭 타입도 결국은 기본 타입인 배열을 사용해 구현해야하며, HashMap 같은 제네릭 타입은 성능을 높일 목적으로 배열을 사용하기도 합니다.
'스터디 > 이펙티브 자바' 카테고리의 다른 글
[Effective Java] Item31. 한정적 와일드카드를 사용해 API 유연성을 높여라 (0) | 2022.06.18 |
---|---|
[Effective Java] Item30. 이왕이면 제네릭 메서드로 (0) | 2022.06.18 |
[Effective Java] Item28. 배열보다는 리스트 (0) | 2022.06.12 |
[Effective Java] Item27. 비검사 경고를 제거 (0) | 2022.06.11 |
[Effective Java] Item26. 로(raw) 타입은 사용하지 말라 (0) | 2022.06.11 |