-
Item 29. 이왕이면 제네릭 타입으로 만들라Book/Effective Java 3E 2022. 11. 12. 10:30반응형
Item 7의 스택 코드는 제네릭 타입이어야 마땅합니다. 제네릭으로 바꾼다고 해도 현재 버전을 사용하는 클라이언트에는 아무런 해가 없습니다. 오히려 지금 상태에서의 클라이언트는 스택에서 꺼낸 객체를 형변환해야 하는데, 이때 런타임 오류가날 위험이 있습니다.
일반 클래스를 제네릭 클래스로 만드는 첫 단계는 클래스 선언에 타입 매개변수를 추가하는 일입니다. 이때 타입 이름으로는 보통 E를 사용합니다(Item 68). 그런 다음 Object를 적절한 타입 매개변수로 바꾸고 컴파일해봅시다. 이 단계에서 대체로 하나 이상의 오류나 경고가 발생합니다(Stack 클래스 생성자 부분: elements = new E[DEFAULT_INITIAL_CAPACITY];). E와 같은 실체화 불가 타입으로는 배열을 만들 수 없기 때문입니다.
첫번째 해결책은 Object 배열을 생성한 다음 제네릭 배열로 형변환하는 것입니다. 이제 컴파일러는 오류 대신 경고를 내보낼 것입니다. 비검사 형변환이 안전하다면 범위를 최소로 좁혀 @SuppressWarnings 애너테이션으로 해당 경고를 숨깁니다. 이 예에서는 생성자가 비검사 배열 생성 말고는 하는 일이 없으니 생성자 전체에서 경고를 숨겨도 좋습니다. 애너테이션을 달면 Stack은 깔끔히 컴파일되고, 명시적으로 형변환하지 않아도 ClassCastException 걱정 없이 사용할 수 있게 됩니다.
package effectivejava.chapter5.item29.technqiue1; import effectivejava.chapter5.item29.EmptyStackException; import java.util.Arrays; // E[]를 이용한 제네릭 스택 public class Stack<E> { private E[] elements; private int size = 0; private static final int DEFAULT_INITIAL_CAPACITY = 16; // 코드 29-1 배열을 사용한 코드를 제네릭으로 만드는 방법 1 // 배열 elements는 push(E)로 넘어온 E 인스턴스만 담는다. // 따라서 타입 안전성을 보장하지만, // 이 배열의 런타임 타입은 E[]가 아닌 Object[]다! @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); } // 코드 29-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()); } }
제네릭 배열 생성 오류를 해결하는 두 번째 방법은 elements 필드의 타입을 E[]에서 Object[]로 바꾸는 것입니다.
package effectivejava.chapter5.item29.technqiue2; import java.util.Arrays; import effectivejava.chapter5.item29.EmptyStackException; // Object[]를 이용한 제네릭 Stack 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; } // 코드 29-2 배열을 사용한 코드를 제네릭으로 만드는 방법 2 // 비검사 경고를 적절히 숨긴다. public E pop() { if (size == 0) throw new EmptyStackException(); // push에서 E 타입만 허용하므로 이 형변환은 안전하다. @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); } // 코드 29-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()); } }
첫번째 방법이 코드도 짧고 가독성도 좋습니다. 보통의 제네릭 클래스라면 코드 이곳저곳에서 이 배열을 자주 사용할 것입니다. 첫 번째 방식에서는 형변환을 배열 생성 시 단 한 번만 해주면 되지만, 두 번째 방식에서는 배열에서 원소를 읽을 때마다 해줘야 합니다. 따라서 현업에서는 첫 번째 방식을 더 선호하며 자주 사용합니다. 하지만 (E가 Object가 아닌 한) 배열의 런타임 타입이 컴파일타임 타입과 달라 힙 오염(heap pollution; Item 32)을 일으킵니다. 힙 오염이 맘에 걸리는 프로그래머는 두 번째 방식을 고수하기도 합니다.
지금까지 설명한 Stack 예는 "배열보다는 리스트를 우선하라"는 Item 28과 모순돼 보입니다. 사실 제네릭 타입 안에서 리스트를 사용하는 게 항상 가능하지도, 꼭 더 좋은 것도 아닙니다. 자바가 리스트를 기본 타입으로 제공하지 않으므로 ArrayList 같은 제네릭 타입도 결국은 기본 타입인 배열을 사용해 구현해야 합니다. 또한 HashMap 같은 제네릭 타입은 성능을 높일 목적으로 배열을 사용하기도 합니다.
타입 매개변수에 제약을 두는 제네릭 타입도 있습니다. 예컨대 java.util.concurrent.DelayQueue는 다음처럼 선언되어 있습니다.
class DelayQueue<E extends Delayed> implements BlockingQueue<E>
타입 매개변수 목록인 <E extends Delayed>는 java.util.concurrent.Delayed의 하위 타입만 받는다는 뜻입니다. 이러한 타입 매개변수 E를 한정적 타입 매개변수(bounded type parameter)라 합니다.
클라이언트에서 직접 형변환해야 하는 타입보다 제네릭 타입이 더 안전하고 쓰기 편하다.
그러니 새로운 타입을 설계할 때는 형변환 없이도 사용할 수 있도록 하라.
그렇게 하려면 제네릭 타입으로 만들어야 할 경우가 많다.
기존 타입 중 제네릭이었어야 하는 게 있다면 제네릭 타입으로 변경하자.
기존 클라이언트에는 아무 영향을 주지 않으면서, 새로운 사용자를 훨씬 편하게 해주는 길이다.
[참고 정보]
이펙티브 자바 Effective Java 3/E 도서 [조슈아 블로크 저]
반응형'Book > Effective Java 3E' 카테고리의 다른 글
Item 31. 한정적 와일드카드를 사용해 API 유연성을 높이라 (0) 2022.11.19 Item 30. 이왕이면 제네릭 메서드로 만들라 (0) 2022.11.12 Item 28. 배열보다는 리스트를 사용하라 (0) 2022.11.12 Item 27. 비검사 경고를 제거하라 (0) 2022.11.08 Item 26. 로 타입은 사용하지 말라 (0) 2022.11.08