본문 바로가기
책/effective java

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

by 2245 2023. 8. 28.

결론

새로운 타입을 설계할 때는 형변환 없이 사용할 수 있는 제네릭 타입으로 만드는 것이 좋습니다.
기존 타입 중 제네릭이었어야 하는 게 있다면 제네릭 타입으로 변경합시다. 
기존 클라이언트에는 아무 영향을 주지 않으면서, 새로운 사용자를 훨씬 편하게 해주는 길입니다. 

 

 

설명

아이템 7에서 다룬 스택 클래스는 원해 제네릭 타입이어야 마땅합니다. 그러니 제네릭으로 만들어봅시다.

 

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

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 class Stack<E> {
    private E[] elements;
    private int size = 0;
    private static final int DEFAULT_INITIAL_CAPACITY = 16;

    public Stack() {
        elements = new E[DEFAULT_INITIAL_CAPACITY];		//컴파일 오류 발생. E와 같은 실체화 불가 타입으로 배열 생성 불가능
    }

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

 

컴파일 오류 발생

E와 같은 실체화 불가 타입으로는 배열을 생성할 수 없습니다.

elements = new E[DEFAULT_INITIAL_CAPACITY]; 

 

 

해결책 1. Object 배열을 생성 후 제네릭 배열로 형변환

elements = (E[]) new Object[DEFAULT_INITIAL_CAPACITY];    //unchecked cast 경고 발생

이 방법은 일반적으로 타입 안전하지 않습니다. 

컴파일러는 이 프로그램이 타입 안전한지 증명할 방법이 없지만, 우리는 이 비검사 형변환이 프로그램의 타입 안전성을 해치지 않음을 확인해 @SuppressWarnings 애노테이션으로 해당 경고를 숨길 수 있습니다. 

 

타입 안전성 검증

  • elements는 private 필드에 저장이 되고, 클라이언트로 반환되거나 다른 메서드에 전달되는 일이 없습니다. 
  • push 메서드를 통해 배열에 저장되는 원소의 타입은 항상 E입니다.
  • 따라서 이 비검사 형변환은 확실히 안전합니다. 

 

@SuppressWarnings 애노테이션

이 예에서는 비검사 배열 생성 말고는 하는 일이 없으므로 생성자 전체에 경고를 숨겨도 좋습니다.

애너테이션을 달면 Stack은 깔끔히 컴파일 되고, 명시적으로 형변환하지 않아도 ClassCastException 걱정 없이 사용할 수 있습니다. 

 

// 배열 elements는 push(E)로 넘어온 E 인스턴스만 담는다.
// 따라서 타입 안전성을 보장하지만,
// 이 배열의 런타임 타입은 E[]가 아닌 Object[]다!
@SuppressWarnings("unchecked")
public Stack() {
    elements = (E[]) new Object[DEFAULT_INITIAL_CAPACITY];
}

 

해결책 2. elements 필드 타입을 E[]에서 Object[]로 변환

private Object[] elements;

 

incompatible types 컴파일 에러 발생

E result = elements[--size];    //incompatible types 컴파일 에러

Object를 E로 명시적 형변환 필요

 

unchecked cast 경고 발생

E result = (E) elements[--size];   //unchecked cast 경고
  • E는 실체화 불가 타입이므로, 컴파일러는 런타임에 이뤄지는 형변환이 안전한지 증명할 방법이 없습니다. 
  • 이번에도 마찬가지로 우리가 직접 증명하고 @SuppressingWarnings 애노테이션을 사용해 숨길 수 있습니다. 

 

@SuppressingWarnings 애노테이션

// 비검사 경고를 적절히 숨긴다.
public E pop() {
    if (size == 0)
        throw new EmptyStackException();

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

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

 

해결책 1 vs 해결책 2

해결책 1 전체 코드

// E[]를 이용한 제네릭 스택
public class Stack<E> {
    private E[] elements;
    private int size = 0;
    private static final int DEFAULT_INITIAL_CAPACITY = 16;

    // 배열 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;
    }

}

 

해결책 2 전체 코드

// Object[]를 이용한 제네릭 Stack
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;
    }

    // 비검사 경고를 적절히 숨긴다.
    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;
    }
}

 

 

두 방법 모두 나름의 지지를 얻고 있습니다.

해결책 1의 장점

  • 가독성이 더 좋습니다.
  • 배열의 타입을 E[]로 선언하여 오직 E 타입 인스턴스만 받음을 확실히 어필합니다.
  • 코드도 더 짧습니다.
  • 1번은 형변환을 배열 생성 시에 한 번만 해주면 되지만, 2번은 배열에서 원소를 읽을 때마다 해주어야 합니다.

 

따라서 현업에서는 첫 번째 방식을 더 선호하며 자주 사용합니다.

하지만, E가 Object가 아닌 한 배열의 런타임 타입이 컴파일타임과 달라 힙 오염(heap pollution)을 일으킵니다.

힙 오염이 마음에 걸리는 프로그래머는 두 번째 방식을 고수하기도 합니다. (이번 아이템의 예에서는 힙 오염이 해가 되지 않습니다.)

 

 

item 28. 배열보다는 리스트를 사용해라?

현재 예제는 item 28의 "배열보다는 리스트를 우선해라"와 모순되어 보입니다. 

사실 제네릭 타입 안에서 리스트를 사용하는 게 항상 가능하지도, 꼭 더 좋은 것도 아닙니다.

자바가 리스트를 기본 타입으로 제공하지 않으므로, ArrayList 같은 제네릭 타입도 결국은 기본 타입인 배열을 사용해 구현해야 합니다. 

또한, HashMap 같은 제네릭 타입은 성능을 높일 목적으로 배열을 사용하기도 합니다.

 

타입 매개변수에 제약을 두는 제네릭 타입

예를 들어, java.util.concurrent.DelayQueue는 다음처럼선언되어 있습니다.

class DelayQueue<E extends Delayed> implements BlockingQueue<E> 
  • Delayed의 하위 타입만 받는다는 뜻입니다.
  • 따라서 DelayQueue 자신과 DelayQueue를 사용하는 클라이언트는 DelayQueue의 원소에서 형변환없이 바로 Delayed 클래스의 메서드를 호출할 수 있습니다.
  • ClassCastException 걱정은 할 필요가 없습니다. 
  • 이러한 타입 매개변수 E를 한정적 타입 매개변수(bounded type paramter)라 합니다.
  • 참고로, 모든 타입은 자기 자신의 하위 타입이므로 DelayQueue<Delayed>도 사용할 수 있습니다.

 


출처

https://www.yes24.com/Product/Goods/65551284

 

이펙티브 자바 Effective Java 3/E - 예스24

자바 플랫폼 모범 사례 완벽 가이드 - Java 7, 8, 9 대응자바 6 출시 직후 출간된 『이펙티브 자바 2판』 이후로 자바는 커다란 변화를 겪었다. 그래서 졸트상에 빛나는 이 책도 자바 언어와 라이브

www.yes24.com