[item 29] 이왕이면 제네릭 타입으로 만들라

김동훈·2023년 7월 23일
1

Effective-java

목록 보기
11/14

Object 기반 Stack

먼저 이전에 작성했던 Stack을 확인해보자

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

    private void ensureCapacity() {
        if (size >= elements.length) {
            throw new ArrayIndexOutOfBoundsException();
        }
    }

    public Object pop() {
        if (size == 0) {
            throw new EmptyStackException();
        }
        Object result = elements[--size];
        elements[size] = null;
        return result;
    }

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

}

지금 이 Stack은 위험이 내재되어있다. 런타임에 ClassCastException등의 오류가 발생 할 수 있다. 제네릭 타입으로 바꿔보자.

Generic 기반 Stack #1

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

    public Stack() { // 비검사 경고 발생. unchecked cast
        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 static void main(String[] args) {
        Stack<String> stack = new Stack<>();
        for (String arg : args)
            stack.push(arg);
        while (!stack.isEmpty())
            System.out.println(stack.pop().toUpperCase());
    }
}

위 Stack에서 봐야할 점은 2가지다.

일반적으로 다운캐스팅은 ClassCastException이 발생한다. String s = (String) new Object(); 와 같이 다운캐스팅하면 말이다. 하지만 생성자에 사용된 다운캐스팅은 예외가 발생하지는 않는다. 이에 대해 말하면, Object배열을 아직 모르는 타입의 배열로 다운캐스팅하 꼴이다. 그러니, unchecked cast라는 비검사 경고를 보여주는것이다.

하지만 우리는 이 비검사 경고에 대해 타입 안전함을 보장할 수 있다. push메소드에서 항상 E타입의 원소를 저장하고 있다. 따라서 배열에 저장될 원소들의 타입은 E 타입임이 확실하므로 @SuppressWarnings("unchecked") 로 경고를 숨겨도 된다.

  • 힙 오염

힙 오염은 item32에서 다루고있는 내용이다. 먼저 간단히 말해서 힙 오염은, a variable of parameterized type points to an object that is not of that parameterized type. 즉, 매개변수화 타입의 변수가 타입이 다른 객체를 참조하면 힙 오염이 발생한다.

위 elements 배열은 런타임에는 Object 배열이다. 하지만 컴파일타임에는 E[]배열이었다. 그러니, 런타임에 E[] 배열이 Object배열을 참조하는 힙 오염이 발생한다. 책에서는 위 코드에는 힙 오염이 해가 되지 않았다고 한다.

그럼 해가 되게끔 코드를 수정해보자!

  1. 우리가 만든 Stack에서 발생한 힙 오염의 원인은 배열의 런타임 타입이 컴파일타임과 다른 Object라는 것이다.
  2. Object타입이라 함은, 모든 타입을 수용한다.
  3. 그럼 String타입 Stack에, Integer타입을 넣는 행위를 유도할 수 있다.
  4. 이러한 행위는 컴파일타임에는 통과되고, 런타임에 문제가 생길 것이다.

코드로 바꾸면!

        Stack<String> stringSt = new Stack<>();
        Stack<Integer> intSt = (Stack<Integer>) (Object) stringSt; // Stack 으로부터 발생된 힙 오염!
        intSt.push(1);
        intSt.push(2);
        String pop = stringSt.pop(); //ClassCastException
        System.out.println("pop = " + pop);

intSt과 stringSt는 동일한 Stack이다. intSt.push(1);로 String타입 Stack에 Integer를 넣을 수 있다. 그 이유는, 런타임에는 Object[]배열이라는 것이다. 제네릭은 컴파일타임에 타입을 지정하여 런타임시 발생할 수 있는 오류를 줄이는데에 목적이 있다. 그런데 우리가 Stack<Integer> intSt = (Stack<Integer>)(Object) stringSt 라는 코드로 컴파일러가 stringSt변수가 참조하는 Stack객체의 타입을 올바르게 지정하였음에도 불구하고, 런타임에 Integer가 들어가게끔 만들어버린것이다.

따라서 런타임에 ClassCastException 가 발생하는 를 만들었다!

Generic 기반 Stack #2

class Stack<E> {
    private Object[] elements; // case 2
    private int size = 0;
    private static final int DEFAULT_INITIAL_CAPACITY = 16;

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

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

    private void ensureCapacity() {
        if (size >= elements.length) {
            throw new ArrayIndexOutOfBoundsException();
        }
    }

    public E pop() {
        if (size == 0) {
            throw new EmptyStackException();
        }
        @SuppressWarnings("unchecked") E result = (E) elements[--size]; // case 2
        elements[size] = null;
        return result;
    }

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

두 번째 방법은 내부에 사용되는 배열의 타입을 E[] -> Object[]로 바꾸는 것이다. 이 방법의 단점은 원소 하나를 꺼낼 때마다 형변환을 해야한다는 점이다. 형변환은 DownCasting이라면 좀 더 느리다고는 한다. 하지만 형변환을 지양해야하는 이유는 유지보수성 측면에 있다고 한다.


effective-java스터디에서 공유하고 있는 전체 item에 대한 정리글

참고

profile
董訓은 영어로 mentor

0개의 댓글