반응형
이 포스팅에서 작성하는 내용은 EFFECTIVE JAVA(이펙티브자바) 에서 발췌하였습니다.
아이템 29. 이왕이면 제네릭 타입으로 만들라
JDK가 제공하는 제네릭 타입과 메서드를 사용하는 건 쉬운 편이지만, 제네릭 타입을 새로 만드는 일은 조금 더 어렵다.
ex 1) Object 기반 Stack. (제네릭이 필요함)
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를 사용
ex 2) 제네릭으로 변경한 소스(컴파일 X)
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;
}
... // isEmpty와 ensureCapacity 메서드는 그대로다.
}
이 단계에서 대체로 하나 이상의 오류나 경고가 발생한다.
위의 코드에서는 아래의 오류가 발생한다.
Stack.java:8: generic array creation
elements = new E[DEFAULT_INITIAL_CAPACITY];
- 아이템 28 내용처럼, E와 같은 실체화 불가 타입으로는 배열을 만들 수 없다.
- 배열을 사용하는 코드를 제네릭으로 만들기 위한 적절한 해결책은 2가지가 있다.
- 첫 번째는 제네릭 배열 생성을 금지하는 제약을 대놓고 우회하는 방법
- 두 번째는 elements 필드의 타입을 E[]에서 Object[]로 바꾸는 것
1) 제네릭 배열 생성을 금지하는 제약을 대놓고 우회하는 방법
// 배열 elements는 push(E)로 넘어온 E 인스턴스만 담는다.
// 따라서 타입 안정성을 보장하지만,
// 이 배열의 런타임 타입은 E[]가 아닌 Object[]다!
@SuppressWarnings("unchecked")
public Stack() {
elements = (E[]) new Object[DEFAULT_INITIAL_CAPACITY];
}
- Object 배열을 생성한 다음 제네릭 배열로 형변환
- 컴파일러는 오류 대신 경고를 보냄
- 컴파일러는 해당 코드가 안전한 지 증명할 방법이 없어, 우리 스스로 확인해야 한다.
문제의 배열 elements는 private 필드에 저장되고, 클라이언트로 반환되거나 다른 메서드에 전달되는 일이 전혀 없고, push 메서드를 통해 배열에 저장되는 원소의 타입은 항상 E다. - 비검사 형변환이 안전함을 직접 증명했다면, 범위를 최소로 좁혀 @SuppressWarnings Annotation을 적용한다.
(아이템 27)
2) elements 필드의 타입을 E[]에서 Object[]로 바꾸는 것
// 비검사 경고를 적절히 숨긴다
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는 실체화 불가 타입이므로 컴파일러는 런타임에 이뤄지는 형변환이 안전한 지 증명할 수 없어, 이번에도 우리가 직접 증명하고 경고를 숨길 수 있다. (@SuppressWarnings Annotation. 아이템 27)
- pop 메서드 전체에서 경고를 숨기지 말고, 아이템 27의 조언을 따라 비검사 형변환을 수행하는 할당문에서만 숨기자
첫 번째 방식 vs 두 번째 방식
- 첫 번째 방식
- 가독성이 더 좋다.
- 배열의 타입을 E[]로 선언하여 오직 E 타입 인스턴스만 받음을 확실히 알 수 있다.
- 보통의 제네릭 코드라면 코드 이곳저곳에서 이 배열을 자주 사용할 것이다.
- 형 변환을 배열 생성 시 단 한번 만 해주면 된다.
- 현업에서는 첫 번째 방식을 더욱 선호한다.
- (E가 Object가 아닌 한) 배열의 런타임 타입이 컴파일타임 타입과 달라 힙 오염을 일으킨다.
- 두 번째 방식
- 형변환을 배열에서 원소를 읽을 때마다 해줘야 한다.
- 힙 오염을 막을 수도 있다.
ex 3) 위에서 제네릭 Stack을 사용하는 예
public static void main(String[] args) {
Stack<String> stack = new Stack<>();
for (String arg : args)
stack.push(arg);
while (!stack.isEmpty())
System.out.println(stack.pop().toUpperCase());
}
정리
- 클라이언트에서 직접 형변환해야 하는 타입보다 제네릭 타입이 더 안전하고 쓰기 편하다.
- 그러니, 새로운 타입을 설계할 때는 형변환 없이도 사용할 수 있도록 하며
이처럼 하려면 제네릭 타입으로 만들어야 할 경우가 많다. - 기존 타입 중 제네릭이었어야 하는 게 있다면, 제네릭 타입으로 변경해주자.
- 기존 클라이언트에는 아무 영향을 주지 않으면서, 새로운 사용자를 훨씬 편하게 해준다.
반응형
'BE > Java' 카테고리의 다른 글
[Effective Java] 아이템 31. 한정적 와일드카드를 사용해 API 유연성을 높이라 (1) | 2022.10.18 |
---|---|
[Effective Java] 아이템 30. 이왕이면 제네릭 메서드로 만들라 (1) | 2022.10.13 |
[Effective Java] 아이템 28. 배열보다는 리스트를 사용하라 (0) | 2022.10.11 |
[Effective Java] 아이템 27. 비검사 경고를 제거하라 (0) | 2022.10.07 |
[Effective Java] 아이템 26. 로 타입은 사용하지 말라 (0) | 2022.10.06 |