제네릭 타입의 클래스를 생성하는 것
• Java JDK에서 제공되는 제네릭 타입의 클래스 메소드를 사용하는 것은 크게 어렵지 않다.
• 그럼 직접 제네릭 타입의 클래스를 선언하는 것은 어떨까?
• 이번 아이템과 함께해줄 코드를 소개합니다! (아이템 7에서 다룬 Object 기반 스택 코드)
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);
}
}
• 제네릭 타입을 사용하지 않고 단순히 Object 라는 최상위 클래스의 범용성을 이용해 구현했다.
• 스택 POP시 Object 타입의 객체를 반환하기 때문에 객체를 꺼낸 후 형변환을 시켜줘야한다.
◦ 형변환 과정에서 런타임 오류가 발생할 수 있음
◦ 제네릭 타입으로 구현해주는 것이 안전하다.
일반 클래스를 제네릭 클래스로 만드는 법
• 먼저 클래스 선언에 타입 매개변수를 추가해줘야 한다.
◦ 스택이 담을 원소의 타입을 하나 추가해주자.
- 타입 이름으로는 보통 E를 사용한다.
• 그 다음 코드에 쓰인 Object 자료형을 적절한 타입 매개변수 자료형으로 바꿔주어야 한다.
◦ Object → E
public class Stack {
private Object[] elements;
...
public Stack() { elements = new Object[DEFAULT_INITIAL_CAPACITY]; }
...
}
public class Stack<E> {
private E[] elements;
...
public Stack() { elements = new E[DEFAULT_INITIAL_CAPACITY]; } // 컴파일 에러 발생
...
}
• 이 때 E 같이 실체화가 불가능한 타입으로는 배열을 만들 수 없어 오류가 발생한다.
해결방법 1 : 제네릭 배열 생성을 금지하는 제약을 우회하는 방법
• Object 배열을 생성한 다음 제네릭 배열로 형변환하면 오류가 경고로 바뀐다.
◦ 컴파일러는 이 프로그램의 타입 안전성을 증명할 방법이 없음 : 직접 확인해줘야 한다.
- 현재 코드의 배열 elements는 private 필드에 저장되므로 외부에 반환되는 일이 없다.
- 즉 push 메소드를 통해 배열에 저장되는 원소의 타입은 항상 E 이므로 이 비검사 형변환은 확실히 안전하다.
◦ 비검사 형변환이 안전함을 직접 증명했다면 @SuppressWarnings 어노테이션으로 해당 경고를 숨기면 된다.
- 현재 예시에서는 생성자가 비검사 배열 생성 말고는 하는 일이 없으므로 생성자 전체에서 경고를 숨겨도 된다.
• 이렇게 어노테이션을 사용하면 오류나 경고 없이 컴파일되고, ClassCastException 발생 걱정도 없다!
// 배열 elements는 push(E)로 넘어온 E 인스턴스만 담는다.
// 타입 안정성을 보장하지만 Object 배열을 생성한 다음 제네릭 배열로 형변환한 것이기 때문에
// 이 배열의 런타임 타입은 E[]가 아닌 Object[] 이다.
@SuppressWarnings("unckecked")
public Stack() {
elements = (E[]) new Object[DEFAULT_INITIAL_CAPACITY];
}
해결방법 2 : elements 필드의 타입을 E[...] 에서 Objects[...] 로 바꾸는 방법
• 이렇게 하면 배열이 반환한 원소를 E로 형변환했을 때 오류 대신 경고가 뜬다.
• E는 실체화 불가 타입이므로 컴파일러는 런타임에 이뤄지는 형변환이 안전한지 증명할 방법이 없다.
◦ 즉 이번에도 개발자가 직접 안정성을 확인하고 경고를 숨겨주는 것이 좋다.
• pop 메소드 전체에서 경고를 숨기지 말고, 비검사 형변환을 수행하는 할당문에서만 숨겨보자(by Item27)
// 비검사 경고를 적절히 숨긴다.
public E pop() {
if (size == 0) throw new EmptyStackException();
// push에서 E 타입만 허용하므로 이 형변환은 안전하다.
@SuppressWarnings("unckecked") E result = (E)elements[--size];
elements[size] = null; // 다 쓴 참조 해제
return result;
}
두가지 방법의 비교
• 첫번째 방법이 가독성이 더 좋다.
◦ 배열의 타임을 E[]로 선언하여 오직 E 타입 인스턴스만 받음을 확실히 어필할 수 있고 코드도 더 짧다.
◦ 첫번째 방식에서는 형변환을 배열 생성 시 단 한번만 해주면 되지만, 두번째 방법에서는 배열에서 원소를 읽을 때마다 해줘야 한다.
◦ 현업에서는 첫번째 방식이 더 많이 사용된다.
◦ 그러나 Heap Pollution(힙 오염)을 일으킨다.
- 배열의 런타임 타입이 컴파일 타입과 다르기 때문
- 힙 오염을 피하고자 두번째 방법이 채택되는 경우가 많다.
제네릭 타입과 배열 vs 리스트
• 지금까지 설명된 Stack 예시는 계속 배열 사용중 → Item 28 : 배열보다는 리스트를 우선하라 와 모순되어 보일 수 있다.
• 제네릭 타입의 경우 리스트를 사용하는것이 항상 가능하지도 않고, 항상 더 좋다고 이야기 할 수도 없다.
◦ Java가 리스트를 기본 타입으로 제공하지 않기 때문에 ArrayList 같은 제네릭 타입도 결국 기본 타입인 배열을 사용하여
구현해야 한다.
◦ HashMap 같은 제네릭 타입은 성능을 높일 목적으로 배열을 사용하기도 한다.
• 대다수의 제네릭 타입은 타입 매개변수에 아무런 제약을 두지 않는다.
◦ Stack<Object>, Stack<int[]>, Stack<List<String>>, Stack 등 어떤 참조 타임으로도 Stack을 만들 수 있다.
◦ 단 기본 타입은 사용할 수 없다. Stack<int>, Stack<double>을 만들려고 하면 컴파일 오류가 난다.
◦ 이는 자바 제네릭 타입 시스템의 근본적인 문제이지만, 박싱된 기본 타입(Integer, Double)을 사용해 우회할 수 있다.
• 타입 매개변수에 제약을 두는 제네릭 타입도 있다.
◦ 예) java.util.concurrent.DelayQueue
class DelayQueue<E extends Delayed> implements BlockingQueue<E>
- 타입 매개변수 목록인 <E extends Delayed>는 java.util.concurrent.Delayed의 하위 타입만 받는다는 뜻이다.
- 이렇게 ehlaus DelayQueue 자신과 DelayQueue를 사용하는 클래스는 DelayQueue의 원소에서 형변환없이 곧바로
Delyed 클래스의 메소드를 호출할 수 있다.
- ClassCastException 발생 걱정은 하지 않아도 된다.
◦ 이러한 타입 매개변수 E 를 한정적 타입 매개변수(bounded type parameter)라고 한다.
◦ 모든 타입은 자기 자신의 하위 타입이므로 DelayQueue<Delayed>로 사용할 수 있다.
'Java' 카테고리의 다른 글
[Effective Java] ITEM39 : 명명 패턴보다 Annotation을 사용하라 (0) | 2022.08.06 |
---|---|
[Effective Java] ITEM34 : int 상수 대신 열거 타입을 사용하라 (0) | 2022.08.06 |
[Effective Java] ITEM24 : 멤버 클래스는 되도록 static으로 만들라 (0) | 2022.07.31 |
[Effective Java] ITEM19 : 상속을 고려해 설계하고 문서화하라. 그러지 않았다면 상속을 금지하라 (0) | 2022.07.30 |
[Effective Java] ITEM14 : Comparable을 구현할지 고려하라 (0) | 2022.07.30 |