본문 바로가기
JAVA/Effective Java

item 29. 이왕이면 제네릭 타입으로 만들어라

by Garonguri 2022. 5. 13.
728x90

예제를 통한 이해...

item 7에서 다뤘던 Object기반 스택 클래스를 제네릭 타입으로 바꿔서 만들어보자...

 

1. 기본

// Can you spot the "memory leak"?
public class Stack {
    private Object[] elements;
    private int size = 0;
    private static final int DEFAULT_INITIAL_CAPACITY = 16;

    public Stack() {
        this.elements = new Object[DEFAULT_INITIAL_CAPACITY];
    }
    public void push(Object e) {
        this.ensureCapacity();
        this.elements[size++] = e;
    }

    /*
    -------------- Bad ------------------
    public Object pop() {
        if (size == 0) {
            throw new EmptyStackException();
        }

        return this.elements[--size];
    }
    */

    /*-------------- Good ------------------*/
    public Object pop() {
        if (size == 0) {
            throw new EmptyStackException();
        }

        Object value = this.elements[--size];
        this.elements[size] = null;
        return value;
    }

    /***
     * 원소를 위한 공간을 적어도 하나 이상 확보한다.
     * 배열 크기를 늘려야 할 때마다 배열의 크기가 대략 2배씩 늘어난다.
     */

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

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

    public static void main(String[] args) {
        System.gc();
        System.runFinalization();
    }
}

Step 1. Class 선언에 타입 매개변수를 추가한다. -> 오류가 난다.

(스택이 담을 원소의 타입 하나만 추가한다.)

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

    public Stack() {
        this.elements = new E[DEFAULT_INITIAL_CAPACITY];
        "여기서 예외가 발생했다.
        E와 같은 실체화 불가 타입으로는 배열을 만들 수 없기 때문이다."
        
        "
        1. 제네릭 배열 생성을 금지하는 제약을 우회하는 방법
        2. elements 필드의 타입을 Object[]로 바꾸는 방법
        
        두 가지 방법으로 오류를 해결할 수 있다.
        "
    }
    public void push(E e) {
        this.ensureCapacity();
        this.elements[size++] = e;
    }

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

        E value = this.elements[--size];
        this.elements[size] = null;
        return value;
    }

}

오류 해결 방법은 2가지.

 

1. 배열 생성 금지 제약을 우회하기.

Object 배열을 생성한 다음 제네릭 배열로 형변환 하는 방식으로 비검사 형변환을 한다.

 

첫 번째 방식을 사용해 오류를 해결한 경우

@SuppressWarning("unchecked")
public Stack() {
    this.elements = (E[]) new Object[DEFAULT_INITIAL_CAPACITY];
    //elements는 push를 통해 들어온 E instance만 받는다.
    //타입 안정성을 보장하지만, 런타임 타입이 E[]가 아닌 Object[]이다.
}

"
타입 안정하지 않은 방법이다.
비검사 형변환이 안전한지 컴파일러는 모른다. 프로그래머가 직접 확인해야 한다.

안전함을 증명했다면 범위를 최소로 해 @SuppressWarning Annotation으로 경고를 숨긴다.
"

장점

- 배열 타입을 E[]로 선언했기 때문에, E 타입의 인스턴스만 받은다는 의미를 정확히 전달해, 가독성이 좋다.

- 코드도 더 짧다.

- 형 변환은 배열이 생성될 때 한 번만 해주면 된다.

-> 이와 같은 이유로 현업에서 더 자주 쓰인다.

 

단점

- E가 Object가 아닌 경우 런타임 타입과 컴파일 타입이 다르다. 따라서 힙 오염을 일으킬 수 있다.


2. elements 필드 타입을 Object[]로 바꾸기

 

두 번째 방식을 사용해 오류를 해결한 경우

public class Stack<E> {
    private Object[] elements; "elements field 타입을 Object[]로 바꾼다."
    private int size = 0;
    private static final int DEFAULT_INITIAL_CAPACITY = 16;

    public Stack() {
        this.elements = new Object[DEFAULT_INITIAL_CAPACITY];
    }
    public void push(E e) {
        this.ensureCapacity();
        this.elements[size++] = e;
    }

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

        @SuppressWarnings("unchecked")
        E value = (E) elements[--size]; "배열이 반환한 원소를 E로 형변환한다."
        this.elements[size] = null;
        return value;
        "
        E는 실체화 불가 타입이다.
        따라서 컴파일러는 런타임에 이뤄지는 형변환이 안전한지 모른다.
        따라서 프로그래머가 직접 증명해 경고를 숨기는 과정을 거쳐야 한다.
        메서드 전체가 아닌 범위를 최소로 좁혀 경고를 숨기는 것을 잊지 말아라.
        "
    }
}

장점

- 힙 오염을 해결할 수 있다.

ArrayList.java를 보니 2번째 방법을 사용한 것을 확인할 수 있었다.

왤까?

 


사실 배열보다 리스트를 우선하라고 책에서 말하고 있지만, 항상 그런 경우가 가능한 것은 아니라고 한다.

- 자바에서 리스트를 기본 타입으로 제공하지 않기 때문

- 제네릭이 성능이 조금 더 낮아서 성능을 높이기 위해

 

배열을 사용하기도 한다고 한다.


Stack처럼 대다수의 제네릭 타입은 타입 매개변수에 제약을 두지 않는다.

다양한 참조 타입으로 Stack을 만들 수 있다. ex) Stack<Object>, Stack<int[]> 등.. 

 

그런데?  -> 기본 타입으로는 Stack을 만들 수 없다. Stack<int>, Stack<double>등은 만들 수 없다.

왜?

이는 자바 제네릭 시스템의 문제점이기도 하다.

Java의 제네릭은 완전히 컴파일 타임 구조이다. 컴파일러는 모든 일반적인 용도를 올바른 유형으로 캐스팅한다. 이는 이전 버전의 JVM 런타임과의 호환성을 유지하기위한 것이다.

모든 제네릭 형식은 런타임에 Object로 변환됩니다. 따라서 제네릭으로 사용되는 모든 항목은 Object로 변환 가능해야 하지만, primitive type의 경우에는 그렇지 않다. 따라서 제네릭에는 사용할 수 없는 것이다.

아~ 그럼 어떻게 해결?

-> 박싱된 기본 타입을 사용하면 해결할 수 있다. int 대신 Integer, double 대신 Double...


한정적 타입 매개변수 (Bounded Type Parameter)

제네릭 타입 파라미터의 범위를 제한하는 방법이다.

 

Upper bounded type <extends>

  • 제네릭 타입에 extends 가 들어가는 경우
  • 나 또는 나의 하위 타입만 받을 수 있다.
  • 특정 클래스의 자기 자신 및 서브 클래스만 타입으로 가지도록 하고 싶은 경우 사용
    • ArrayList<? extends ClassA>
    • List<E extends ClassB>
    • 위 예제를 보면, Upper bounded type을 사용함으로서 내부 원소들에서 Number , Delayed 에 해당하는 메서드를 호출할 수 있게 됩니다.
  • 인터페이스, 클래스, 추상 클래스 모두 다 extends를 사용합니다.

 

Lower bounded type <super>

  • 제네릭 타입에 super 가 들어가는 경우
  • 나 또는 나의 상위 타입만 받을 수 있다.
  • 특정 클래스의 자기 자신 및 상위 클래스만 타입으로 가지도록 하고 싶은 경우 사용
    • ArrayList<? super ClassC>
    • List<? super ClassD>
핵심 정리

직접 형변환 해야하는 타입보다 제네릭 타입이 더 안전하고 편하다.
따라서 새로운 타입을 설계해야 한다면 형변환 할 필요가 많이 줄어드는 제네릭 타입을 사용하자.
클라이언트에게도, 너에게도 좋다.

파이팅!
728x90

댓글