[이펙티브 자바] 아이템29 | 이왕이면 제네릭 타입으로 만들라

제롬·2022년 3월 22일
0

일반 클래스를 제네릭 타입으로 변경.

  • 일반 클래스를 제네릭 타입 클래스로 바꾼다고 해도 클라이언트에는 아무런 해가 없다.

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


public class ObjectStack {
    private Object[] elements;
    private int size = 0;
    private static final int DEFAULT_INITIAL_CAPACITY = 16;

    public ObjectStack() {
        this.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);
        }
    }
}
  • 위와 같은 코드 상태로는 스택에서 꺼낸 객체를 형변환해야 하는데 이때 런타임 오류가 발생할 가능성이 있다. 따라서 제네릭 타입 클래스로 바꾸는것이 좋다.

  • 일반 클래스에서 제네릭 타입 클래스로 만드는 시작은 타입 매개변수를 추가하는 것이다. 이때 타입이름으로 보통 E를 사용한다.

[제네릭 스택으로 변경]


public class GenericStack<E> {
    private E[] elements;
    private int size = 0;
    private static final int DEFAULT_INITIAL_CAPACITY = 16;

    public GenericStack() {
        this.elements = new E[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);
        }
    }
}
  • 위 코드의 생성자에서 오류가 발생한다. 아이템 28에서 설명한 것처럼 E와 같은 실체화 불가 타입으로는 배열을 만들 수 없기 때문이다.
  • 위 문제를 해결하는 두 가지 방법이 있다.

타입 매개변수 오류를 해결하는 두가지 방법

1. Object 배열을 생성하고 그 다음 제네릭 배열로 형변환하는 제네릭 배열 생성을 금지하는 제약을 대놓고 우회하는 방법.

[Object 배열 생성 시 배열 형변환]

...
public void push(E e) {
        ensureCapacity();
        elements[size++] = e;
}

public GenericStack() {
    elements = (E[]) new Object[DEFAULT_INITIAL_CAPACITY]; // 경고 메시지 발생
}

private void ensureCapacity() {
    if (elements.length == size) {
        elements = Arrays.copyOf(elements, 2 * size + 1);
    }
}
...
// 비검사 형변환 경고 메시지 발생
Unchecked cast: 'java.lang.Object[]' to 'E[]
  • 위 코드에서는 비검사 형변환 경고 메시지가 발생한다.
  • 위 코드에서 elements 배열은 private 필드에 저장된 후 클라이언트로 반환되거나 다른 메서드에 전달되지 않는다.
  • push 메서드를 통해 배열에 저장되는 원소의 타입은 항상 E이다.
  • 위 두가지 이유로 이 비검사 형변환은 확실히 안전하다.
  • 비검사 형변환 경고를 없애기 위해 범위를 최대한 줄여 @SuppressWarnings("unchecked")를 사용하여 경고를 숨긴다.

[비검사 형변환 경고 숨김]

...
public void push(E e) {
        ensureCapacity();
        elements[size++] = e;
}

// 배열 elements는 push(E)로 넘어온 E인스턴스만 남는다.
// 타입 안정성을 보장하지만 이 배열의 런타임 타입은 Object[] 이다.
@SuppressWarnings("unchecked")
public GenericStack() {
    elements = (E[]) new Object[DEFAULT_INITIAL_CAPACITY]; // 경고 메시지 발생
}

private void ensureCapacity() {
    if (elements.length == size) {
        elements = Arrays.copyOf(elements, 2 * size + 1);
    }
}
...
// 비검사 형변환 경고 메시지 발생
Unchecked cast: 'java.lang.Object[]' to 'E[]
  • 생성자가 배열 생성말고는 따로 하는 기능이 없기 때문에 생성자 전체에서 경고를 숨겨도 된다.

2. elements 필드의 타입을 E[]에서 Object[]로 바꾸는 방법.

[Object 배열은 그대로 두고 pop() 사용시 형변환]

// 비검사 경고를 적절히 숨긴다.
public class GenericStack<E> {
    private Object[] elements;
	...
    public GenericStack() {
        this.elements = new Object[DEFAULT_INITIAL_CAPACITY];
    }

    public void push(E e) {
        ensureCapacity();
        elements[size++] = e;
    }

    public E pop() {
        if (size == 0) {
            throw new EmptyStackException();
        }
		// push에서 E 타입만 허용하므로 이 형변환은 안전하다.
        @SuppressWarnings("unchecked")
        E result = (E) elements[--size];
        elements[size] = null; // 다 쓴 참조 해제

        return result;
    }
	...
}

제네릭 배열 생성을 제거하는 두 방법의 장단점

  • 첫 번째 방법은 가독성이 좋고 코드가 짧다. 또한 배열을 한번만 생성하면 된다. 하지만, EObject가 아니기 때문에 (배열의 컴파일타임의 타입과 런타임의 타입이 다르기 때문에) 힙 오염을 일으킨다.
  • 두 번째 방법은 배열을 원소를 읽을때마다 생성해야 한다.

정리

실체화 불가 타입을 다루는 과정에서 컴파일러의 경고를 우회하는 모순적인 상황이 발생하지만 제네릭 타입이 주는 타입 안정성은 굉장한 편리함을 주기 때문에 제네릭을 적절히 그리고 적극적으로 사용하자.

[Reference]
힙 오염

0개의 댓글