Search
Duplicate

아이템 29 - 이왕이면 제네릭 타입으로 만들라

작성자
챕터
5장 - 제네릭
최종 편집
2023/07/22 05:48
생성 시각
2023/07/22 02:40

제네릭 타입을 새로 만들어보자

Object 기반 스택 코드이다.
public class Stack { private Object[] elements; private int size = 0; private static final int DEFAILT_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); } }
Java
복사
스택에서 꺼낸 객체를 형변환하면서 런타임 오류가 날 위험이 있다. 제네릭으로 만들어보자!

일반 클래스를 제네릭 클래스로 만드는 방법

클래스 선언에 하나 이상의 타입 매개변수를 추가한다. 이 때 타입의 이름으로 보통 E를 사용한다. E와 같은 실체화 불가 타입으로는 배열을 만들 수 없다.
제네릭 스택으로 바꿔봤지만 컴파일 오류가 난다.
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]; // Type parameter 'E' cannot be instantiated directly. } 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 메서드는 그대로 }
Java
복사
제네릭 배열을 만드는 방법 2가지
방법1: Object 배열을 생성한 다음 제네릭 배열로 형변환한다.
방법 2: elements 필드의 타입을 E[]에서 Object[]로 바꾼다.
방법 2와 비교했을 때 방법 1의 장단점
장점
가독성이 더 좋다. 배열의 타입을 E[]로 선언하여 오직 E 타입 인스턴스만 받는다는 것을 확실하게 알 수 있다.
코드도 더 짧다.
방법 1에서는 형 변환을 배열 생성 시 한 번만 해주면 되지만, 방법 2에서는 배열에서 원소를 읽을 때마다 해줘야 한다.
단점
E가 Object가 아닌 한, 배열의 런타임 타입이 컴파일타임의 타입과 달라 heap pollution을 일으킨다. 현재 예시에서는 문제가 되지 않는다.
현업에서는 주로 방법 1을 선호하지만 heap pollution 문제 때문에 방법 2를 사용하기도 한다.
heap pollution은 뒷장에서 나오니 지금은 간단하게 알아보자.
매개변수화 타입이 매개변수화 타입이 아닌 것을 참조할 때 발생하는 현상.
매개변수화 타입은 타입 소거가 이루어진다. 따라서 런타임 시점에는 타입 정보를 모른다.
하지만 매개변수화 타입이 아닌 배열은 런타임 시점에 타입 정보를 안다.
컴파일타임과 런타임의 정보가 달라서, UncheckedWarningClassCastException이 발생.
방법 1과 2 모두 클라이언트에서 elements를 받아올때 형변환하지 않아도 문제가 발생하지 않는다. 컴파일러에 의해 자동 생성된 형변환이 항상 성공함을 보장한다.
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()); }
Java
복사
아이템 28에서는 배열보다 리스트를 우선하라고 했지만, 사실 제네릭 타입 안에서 리스트를 사용하는 게 항상 가능하지도, 꼭 더 좋은 것도 아니다.
자바가 리스트를 기본 타입으로 제공하지 않으므로 ArrayList 같은 제네릭 타입도 결국은 기본 타입인 배열을 사용해 구현해야 한다. 또한, HashMap 같은 제네릭 타입은 성능을 높일 목적으로 배열을 사용하기도 한다.

정리

클라이언트에서 직접 형변환해야 하는 타입보다 제네릭 타입이 더 안전하고 쓰기 편하다.
새로운 타입을 설계할 때는 형변환 없이도 사용할 수 있도록 하자.
기존 타입 중 제네릭이었어야 하는 게 있다면 제네릭 타입으로 변경하자.
제네릭 타입으로 만들 수 없는 경우라면, 형변환을 해도 안전한지 사람이 직접 확인한 후, @SuppressWarnings("unchecked") 어노테이션을 달아 경고를 숨기자.