Search
🙈

이펙티브 자바:: 아이템 29 <이왕이면 제네릭 타입으로 만들라>

Intro::

이펙티브 자바 정리본입니다.

결론

클라이언트에서 직접 형변환해야 하는 타입보다 제네릭 타입이 더 안전하고 쓰기 편합니다.
새로운 타입을 설계할 때는 형변환 없이도 사용할 수 있도록 해야합니다. 그렇게 하기 위해서는 제네릭 타입으로 만들어야 합니다.

배열을 사용한 코드를 제네릭으로

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, 2 * size + 1); } }
Java
복사
위와 같은 Object 배열을 원소 배열로 사용하는 스택 클래스가 있다고 생각해봅시다. 이 클래스는 원래 제네릭 타입이어야 마땅합니다.
이 클래스를 제네릭으로 바꾼다고 해도 현재 버전을 사용하는 클라이언트에는 아무런 해가 없습니다. 오히려 지금 상태에서의 클라이언트는 스택에서 꺼낸 객체를 형변환해야 하는데, 이때 런타임 오류가 날 위험이 있습니다.

방법 1

public class Stack<E> { private E[] elements;// (1) private int size = 0; private static final int DEFAULT_INITIAL_CAPACITY = 16; // 코드 29-3 배열을 사용한 코드를 제네릭으로 만드는 방법 1 (172쪽) // 배열 elements는 push(E)로 넘어온 E 인스턴스만 담는다. // 따라서 타입 안전성을 보장하지만, // 이 배열의 런타임 타입은 E[]가 아닌 Object[]다! @SuppressWarnings("unchecked") public Stack() { elements = (E[]) new Object[DEFAULT_INITIAL_CAPACITY];// (2) } 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; } ... }
Java
복사
(2) 에서 명시적으로 형변환을 해주지 않는다면, E와 같은 실체화 불가 타입으로는 배열을 만들 수 없기 때문에 에러가 발생합니다. 이와 같이 형변환 해주는 방식은 타입 일반적으로 타입 안전하지 않습니다.
컴파일러는 이 프로그램이 타입 안전한지 증명할 방법이 없기 때문에 스스로 확인을 해야합니다. 비검사 형변환이 안전함을 직접 증명했다면 범위를 최소로 좀혀 @SuppressWarnings 애너테이션으로 해당 경고를 숨길 수 있습니다.

장점

가독성이 좋습니다.
코드가 짧습니다.
형변환을 배열 생성시 단한번만 해주면 됩니다.
현업에서 더 선호하는 방식입니다.

단점

런타임 타입과 컴파일 타임 타입이 달라 힙 오염을 일으킵니다.

방법 2

public class Stack<E> { private Object[] elements;// (1) 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-4 배열을 사용한 코드를 제네릭으로 만드는 방법 2 (173쪽) // 비검사 경고를 적절히 숨긴다. public E pop() { if (size == 0) throw new EmptyStackException(); // push에서 E 타입만 허용하므로 이 형변환은 안전하다. @SuppressWarnings("unchecked") E result = (E) elements[--size];// (2) elements[size] = null; // 다 쓴 참조 해제 return result; } ... }
Java
복사
(2) 에서 E로 변환 해주지 않으면 배열이 Object 배열이기 때문에 에러가 발생합니다. E는 실체화 불가 타입으로 컴파일러는 런타임에 이뤄지는 형변환이 안전한지 증명할 방법이 없습니다. 이전 방법과 마찬가지로 직접 타입 안전한지 증명하고 경고를 숨겨주면 됩니다.

장점

힙오염이 없습니다.

단점

상대적으로 코드 가독성이 떨어집니다.
배열에서 원소를 읽을 때 마다 형변환을 해주어야 합니다.

왜 제네릭 타입안에서 배열을 사용하는가???

이전까지의 설명에 따르면 제네릭 타입안에서는 리스트를 사용하는게 좋아보였지만, 사실 제네릭 타입 안에서 리스트를 사용하는게 항상 가능하지도, 꼭 더 좋은 것도 아니라고 합니다. 자바가 리스트를 기본 타입으로 제공하지 않으므로 ArrayList 같은 제네릭 타입도 결국 기본 타입인 배열을 사용해 구현해야 합니다. 또한 HashMap 같은 제네릭 타입은 성능상 목적으로 배열을 사용하기도 합니다.

한정적 타입 매개변수

일반적으로 대다수의 제네릭 타입은 타입 매개변수에 아무런 제약을 두지 않습니다. 단, 기본 타입은 제외합니다(컴파일 오류가 발생).
class DelayQueue<E extends Delayed> implements BlockingQueue<E> 와 같이 java.util.concurrent.Delayed 의 하위 타입만 받는 경우도 존재하고, 이와 같은 타입 매개변수 E를 타입 매개변수라고 합니다.

궁금한점

힙 오염이 발생한다는게 무슨말인가??

힙 오염이란 힙 메모리의 문제가 아니라, 제네릭 타입을 사용할 때 타입 안정성을 해치는 상황을 의미합니다. 이로 인해 발생하는 문제는 주로 런타임에서 잘못된 타입 사용으로 인해 예기치 못한 오류가 발생할 수 있다는 것입니다.
// 방법 1의 Stack 클래스 사용 가정 Stack<String> stringStack = new Stack<>(); Object[] objectArray = stringStack.elements; // 사실 elements의 런타임 타입은 Object[]입니다. objectArray[0] = 42; // 이 코드는 컴파일이 되지만, 런타임에 ClassCastException이 발생할 수 있습니다.
Java
복사

References::

이펙티브 자바 / 조슈아 블로크 지음 (프로그래밍 인사이트)