본문 바로가기
개발관련 서적 정리/Effective Java

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

by Backchus 2023. 2. 5.

어떤 클래스를 만들다 보면 클래스안에 어떤 다른 객체를 담는 역할을하는 클래스를 만들 수 있습니다. 예를 들어 스택이라는 자료구조안에 어떤 Element들을 쌓는경우가 생기는데 이런경우 보통 제네릭타입으로 만들면 유용합니다. 특히 Object타입으로 담고있다면 더더욱 명확하게 제네릭을 사용해서 더 구체적인 타입으로 코딩을 하도록 유도하면 런타임시에 ClassCastException을 많이 줄여줄 수 있습니다.

Object를 이용한 제네릭 스택

package me.whiteship.chapter05.item29.object;

import me.whiteship.chapter05.item29.EmptyStackException;

import java.util.Arrays;
import java.util.List;

// Object를 이용한 제네릭 스택 (170-174쪽)
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;
    }

    public boolean isEmpty() {
        return size == 0;
    }

    private void ensureCapacity() {
        if (elements.length == size)
            elements = Arrays.copyOf(elements, 2 * size + 1);
    }
}

위에 구현한 스택은 Object타입으로 요소들을 받고 있기 때문에 아래와 같이 pop을 할때 Object타입이기 때문에 넣어줬던 타입에 맞게 강제로 타입캐스팅을 해야합니다.

    public static void main(String[] args) {
        Stack stack = new Stack();
        for (String arg : List.of("a", "b", "c")) {
            stack.push(arg);
        }
        while (!stack.isEmpty()) {
            System.out.println(((String) stack.pop()).toUpperCase());
        }
    }

이렇게 강제 타입캐스팅을 하면 String타입이 아니고 다른 타입을 넣었을경우 ClassCastException이 일어날 수 있습니다. 이런 경우를 미연에 방지하기 위해 제네릭으로 스택을 구현하는게 좋은데 2가지 방법이 있습니다.

E[]를 이용한 제네릭 스택

package me.whiteship.chapter05.item29.technqiue1;

import me.whiteship.chapter05.item29.EmptyStackException;

import java.util.Arrays;
import java.util.List;

// E[]를 이용한 제네릭 스택 (170-174쪽)
public class Stack<E> {
    private E[] elements;
    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];
    }

    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);
    }
}
public class Stack<E> {
    private E[] elements;
}

첫번째 방법은 제네릭 타입을 선언하고 Object대신에 제네릭타입의 배열을 선언하고 선언한 변수에 오브젝트 타입의 배열을 생성합니다.

    @SuppressWarnings("unchecked")
    public Stack() {
        elements = (E[]) new Object[DEFAULT_INITIAL_CAPACITY];
    }

이렇게 Object타입의 배열을 선언하는 이유는 new E[DEFAULT_INITIAL_CAPACITY] 이런식으로 생성할 수 없고 컴파일시에 에러가 발생합니다. 따라서 Object배열로 선언 후 제네릭 배열타입으로 타입캐스팅을 하는 방법이 있습니다. 대신 런타임에는 타입캐스팅하는 (E[])부분이 소거되기때문에 Object타입의 배열로 동작합니다. 대신에 pop을 할때 지정한 타입으로 캐스팅없이 바로 꺼낼 수 있다는 장점이 있고 (E[]) new Object[DEFAULT_INITIAL_CAPACITY]; 이 부분에서 런타임경고가 뜨는데 SuppressWarnings어노테이션으로 경고를 무시할 수 있습니다. 하지만 힙오염이 될 가능성이 있습니다. 힙 오염은 아이템 32에서 자세히 다뤄보도록 하겠습니다.

Object[]를 이용한 제네릭 Stack

package me.whiteship.chapter05.item29.technqiue2;

import me.whiteship.chapter05.item29.EmptyStackException;

import java.util.Arrays;
import java.util.List;

// Object[]를 이용한 제네릭 Stack (170-174쪽)
public class Stack<E> {
    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(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];

        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배열로 선언 합니다.

    private Object[] elements;

    public Stack() {
        elements = new Object[DEFAULT_INITIAL_CAPACITY];
    }

그 대신에 제네릭타입으로만 push를 받도록 합니다.

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

pop을 할때 제네릭타입으로 형변환을 합니다.

    public E pop() {
        if (size == 0)
            throw new EmptyStackException();

        // push에서 E 타입만 허용하므로 이 형변환은 안전하다.
        @SuppressWarnings("unchecked") E result = (E) elements[--size];

        elements[size] = null; // 다 쓴 참조 해제
        return result;
    }

이러한 방법의 장점은 힙 오염이 발생할 여지가 없습니다. 하지만 무언가를 꺼낼때마다 해당하는 타입으로 형변환을 해줘야합니다.