본문 바로가기

Java

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

이번장은 제네릭 타입을 만드는 방법에 대해 소개한다.

 

먼저 아이템7. 다 쓴 객체 참조를 해제하라에서 다룬 스택 코드를 살펴보자.

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

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

이 코드는 원래 제네릭 타입인 것이 좋다.

지금 상태에서는 클라이언트는 스택에서 값을 꺼낼 때 마다 객체를  형변환해야 한다. 타입 세이프하지 않을 수 있어 런타임 오류가 날 수 있다.

 

위 일반 클래스를 제네릭 클래스로 만들어 보자.

1) 클래스 선언 타입에 매개변수를 추가

2) Object를 적절한 타입 매개변수로 바꾸기

 

class StackGeneric<E> { //1) 클래스 선언 타입에 매개변수를 추가
    private E[] elements; //2) Object를 적절한 타입 매개변수로 바꾸기
    private int size = 0;
    private static final int DEFAULT_INITIAL_CAPACITY = 16;

    //2) Object를 적절한 타입 매개변수로 바꾸기
    public StackGeneric() {
        elements = new E[DEFAULT_INITIAL_CAPACITY];
    } // 컴파일 에러(java: generic array creation)

    public void push(E e){ //2) Object를 적절한 타입 매개변수로 바꾸기
        ensureCapacity();
        elements[size++] = e;
    }

    public E pop(){ //2) Object를 적절한 타입 매개변수로 바꾸기
        if(size == 0)
            throw new EmptyStackException();
        E result = elements[--size]; //2) Object를 적절한 타입 매개변수로 바꾸기
        elements[size] = null;
        return result;
    }

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

 

생성자 코드에서 아래와 같은 컴파일 에러가 발생한다.

public StackGeneric() {
    elements = new E[DEFAULT_INITIAL_CAPACITY];
}// 컴파일 에러(java: generic array creation)

 

해결방법

1. 제네릭 배열 생성을 금지하는 제약을 우회

public StackGeneric() {
    elements = (E[]) new Object[DEFAULT_INITIAL_CAPACITY];
} // 경고: uses unchecked or unsafe operations.

uses unchecked or unsafe operations.라는 경고가 발생한다.

아이템27.비검사 경고(unchecked warning)를 제거하라에서 배운대로 이 비검사 형변환이 타입 safe하다는 것을 확인하고 경고를 숨겨야 한다.

-> 배열 elements는 private필드에 저장되고, 클라이언트로 반환되거나 다른 메서드에 전달되는 일이 없다.

->  push메서드를 통해 배열에 저장되는 원소의 타입은 항상 E이다

-> 따라서 이 비검사 형변환은 안전하다.

 

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

위처럼 @SuppressWaraings를 달아 경고를 무시한다.

 

 

2. elements필드 타입을 E[] -> Object[]로 바꾼다.

class StackGeneric2<E> { //1) 클래스 선언 타입에 매개변수를 추가
    private Object[] elements; //2) Object를 적절한 타입 매개변수로 바꾸기
    private int size = 0;
    private static final int DEFAULT_INITIAL_CAPACITY = 16;

    //2) Object를 적절한 타입 매개변수로 바꾸기
    @SuppressWarnings("unchecked")
    public StackGeneric2() {
        elements = (E[]) new Object[DEFAULT_INITIAL_CAPACITY];
    }

    public void push(E e){ //2) Object를 적절한 타입 매개변수로 바꾸기
        ensureCapacity();
        elements[size++] = e;
    }

    public E pop(){ //2) Object를 적절한 타입 매개변수로 바꾸기
        if(size == 0)
            throw new EmptyStackException();
        E result = elements[--size]; //2) Object를 적절한 타입 매개변수로 바꾸기
        elements[size] = null;
        return result;
    }

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

1에서 elements 선언시 필드타입을 Object로 바꿔준다.

 

public E pop(){ 
    if(size == 0)
        throw new EmptyStackException();
    E result = elements[--size]; //컴파일 에러: java: incompatible types: java.lang.Object cannot be converted to E
    elements[size] = null;
    return result;
}

그럼 위 부분에서 컴파일 에러가 발생한다.

아래처럼 배열이 반환한 원소를 E로 형변환하면 오류 대신 경고가 발생. 

public E pop(){
    if(size == 0)
        throw new EmptyStackException();
    E result = (E) elements[--size]; // uses unchecked or unsafe operations.
    elements[size] = null;
    return result;
}

 

이 역시 @SuppressWarnings를 붙여 해결한다.

@SuppressWarnings("unchecked")
E result = (E) elements[--size]; // uses unchecked or unsafe operations.

 

 

제너릭 배열 생성을 제거하는 두 방법 모두 장단점이 있다.

1번. 제네릭 배열 생성을 금지하는 제약을 우회

- 가독성이 더 좋다. 배열의 타입을 E[]로 선언해 오직 E타입 인스턴스만 받음을 표현한다.

- 형변환을 배열 생성 시 한 번만 해주면 된다.

- 단점) 배열의 런타임 타입이 컴파일 타입과 달라 힘 오션(아이템32)을 일으킨다.

 

2번. elements필드 타입을 E[] -> Object[]로 바꾼다.

- 힙 오염을 고려하지 않아도 된다.

- 단점) 배열에서 원소를 읽을 때마다 형변환을 해줘야 한다.