JDK가 제공하는 제네릭 타입과 메서드를 사용하는 일은 일반적으로 쉬운 편이지만, 제네릭 타입을 새로 만드는 일은 조금 더 어렵다.
만드는 방법에 대해 알아보자.

Object 기반 스택 - 제네릭이 절실한 강력 후보!


public class Stack {
    public static void main(String[] args) {
        Stack stack = new Stack();
        stack.push("Jinyoung");
        System.out.println(stack.pop());
        System.out.println(stack.isEmpty());

    }
    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);
        }
    }

}

위 클래스는 제네릭 타입이어야 마땅하다. 그러니 제네릭으로 만들어보자.

Generic 기반 스택

  1. 클래스 선언부분에 타입 매개변수 추가(이 때 타입 이름으로는 보통 E를 사용)
  2. code에 쓰인 Object를 적절한 타입 매개변수로 변경
    • 컴파일 에러 발생
    • 해당 에러는 타입캐스팅!
    • 해결책은?
      a) Object배열 생성 후 제네릭 배열로 형변환 - 오류는 나지 않지만 type-safe X - 하지만 push() 메서드를 통해 배열에 저장되는 원소의 타입은 항상 E다. 따라서 확실히 안전하다. - 따라서 @SuppressWarning 로 경고를 숨기자. - 가독성 좋음 - 형 변환을 배열 생성 시 단 한번만 해주면 된다. 따라서 더 선호하는 방식이다. - 하지만, 배열의 런타임 타입이 컴파일타임 타입과 달라 힙 오염을 일으킨다. 이러한 문제의 발생을 고려해야 한다면, 두번째 방식을 쓰기도 한다.

      b) elements필드 타입을 E[] -> Object[]로 변환 - 위의 a 방법과 같이 type-safe X. But push() -> 확실히 안전함 - 따라서 @SuppressWarning 로 경고를 숨기자. - 형 변환을 배열에서 원소를 읽을 때마다 해줘야 한다.

public class GenericStack<E> {

    public static void main(String[] args) {
        GenericStack<Long> stack = new GenericStack<>();
        stack.push((long) 1);
        while (!stack.isEmpty()) {
            System.out.println(stack.pop());
        }
    }
    private E[] elements;
    private int size = 0;
    private static final int DEFAULT_INITIAL_CAPACITY = 16;

    @SuppressWarnings("unchecked") // a 방법 
    public GenericStack() {
        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();
        }

        @SuppressWarnings("unchecked") // b 방법 
        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);
        }
    }

}

이렇게 함으로써 Object기반 Stack을 제네릭으로 바꿀 수 있었다.
이 예시처럼 대다수의 제네릭 타입은 타입 매개변수에 아무런 제약을 두지 않고 만들 수 있다.
단, 기본 타입은 예외다.

아이템 28 “배열보다는 리스트를 우선하라”와의 모순?

이번 주제는 “배열보다는 리스트를 우선하라”라는 이야기와는 모순이 되어 보인다.
하지만 제네릭 타입 안에서 리스트를 사용하는 게 항상 가능하지도, 꼭 더 좋은 것도 아니다.

  • 자바가 리스트를 기본 타입으로 제공하지 않으므로 ArrayList 같은 제네릭 타입도 결국 기본 타입인 배열을 사용해 구현해야 한다.
  • HashMap 같은 제네릭 타입은 성능을 높일 목적으로 배열을 사용하기도 한다.

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

댓글남기기