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

DEINGVELOP·2023년 1월 4일
0

Effective Java

목록 보기
10/19

제네릭 타입 만들기

JDK가 제공하는 제네릭 타입과 메서드를 사용하는 일은 일반적으로 쉬운 편이지만,제네릭 타입을 새로 만드는 일은 조금 더 어렵다. (그래도 배워두면 그만한 값어치는 충분히 한다.)


제네릭 타입 클래스 만들기

아이템 7 - Stack class
: 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) {
        ensureCapacityO;
        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 기반 클래스의 클라이언트는 스택에서 꺼낸 객체를 형변환해야 하는데,이때 런타임 오류가 날 위험이 있다.

제네릭 클래스 만들기 단계

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

    위의 stack 클래스 예시에서는 스택이 담을 원소의 타입 하나만 추가하면 된다. 이때 타입 이름으로는 보통 E를 사용한다(아이템 68).

  2. 그런 다음 코드에 쓰인 Object를 적절한 타입 매개변수로 바꾸고 컴파일해보기

     // 제네릭 스택으로 가는 첫 단계 - 컴파일되지 않는다.
    
     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];
         }
    
         public void push(E e) {
             ensureCapacity();
             elements [size44-] = e;
         }
    
         public E pop() {
             if (size = 0)
                 throw new EmptyStackException();
             E result = elements[—size];
             elements [size] = null; // 다 쓴 참조 해제
             return result;
         }
         ... // isEmpty와 ensureCapacity 메서드는 그대로다.
     }

    이 단계에서 대체로 하나 이상의 오류나 경고가 뜨는데,이 클래스도 예외는 아니다.

    (여기서는 다행히 오류가 하나만 발생했다)

    Stack.java:8: generic array creation
        elements = new E[DEFAULT_INITIAL_CAPACITY];
                   ^

    💡 E[]에서 오류가 나는 이유

    E와 같은 실체화 불가 타입으로는 배열을 만들 수 없다.(아이템 28 참고)
    배열을 사용하는 코드를 제네릭으로 만들려 할 때는 이 문제가 항상 발목을 잡게 된다.

  3. 컴파일 에러 해결하기

    📌 해결책 2가지

    1. 제네릭 배열 생성을 금지하는 제약을 대놓고 우회하는 방법

    ➡ Object 배열을 생성한 다음 제네릭 배열로 형변환하기
    - 컴파일러는 오류 대신 경고를 내보내게 됨

      ```
      Stack.java:8: warning: [unchecked] unchecked cast
      found: Object[], required: E[]
          elements = (E[]) new Object[DEFAULT_INITIAL_CAPACITY];
                         ^
      ```
    - (일반적으로) 타입 안전하지 않다

    💡 그렇다면, 타입 안전하게는 어떻게 할까?

    ➡ 직접 안전성을 확인하기!

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

    • 비검사 형변환이 안전함을 직접 증명했다면 범위를 최소로 좁혀 @SuppressWarnings 애너테이션으로 해당 경고를 숨긴다.
      (이 예시에서는 생성자가 비검사 배열 생성 말고는 하는 일이 없으니 생성자 전체에서 경고를 숨겨도 좋다. 애너테이션을 달면 Stack은 깔끔히 컴파일되고,명시적으로 형변환하지 않아도 ClassCastException 걱정 없이 사용할 수 있게 된다.)

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

    2. elements 필드의 타입을 E[]Objects로 바꾸기

    이렇게 하면 첫번째와는 다른 오류가 발생한다.

    Stack.java:19: incompatible types
    found: Object, required: E
    				E result = elements[--size];
    									^

    배열이 반환한 원소를 E로 형변환하면 오류 대신 경고가 뜬다.

    Stack.java:19: warning: [unchecked] unchecked cast
    		  found: Object, required: E
    				E result = (E) elements[ size];
    		  ```
    
  • E는 실체화 불가 타입 → 컴파일러는 런타임에 이뤄지는 형변환이 안전한지 증명할 방법이 없음

    💡 이번에도 직접 증명을 통해 경고를 숨길 수 있다.
    pop 메서드 전체에서 경고를 숨기지 말고,비검사 형변환을 수행하는 할당문에서만 숨기자.(아이템 27)

    // 비검사 경고를 적절히 숨긴다
    public E pop() {
    	if (size = 0)
    		throw new EmptyStackExceptionf);
    
    	// push에서 E 타입만 허용하므로 이 형변환은 안전하다.
    	@SuppressWarnings("unchecked")
    	E result = (E) elements[--size];
    	
    	elements [size] = null; // 다 쓴 참조 해제
    	return result;
    }

    👉🏻 두 방법 모두 나름의 지지를 얻고 있다. 그러나 현업에서 1번째 방식을 더 선호하며 자주 사용한다.

    첫 번째 방식이 선호되는 이유

    • 배열의 타입을 E[]로 선언하여 오직 E 타입 인스턴스만 받음을 확실히 어필한다.
    • 코드도 더 짧다.
    • 보통의 제네릭 클래스라면 코드 이곳저곳에서 이 배열을 자주 사용할 것이다.
    • 첫 번째 방식에서는 형변환을 배열 생성 시 단 한 번만 해주면 되지만,두 번째 방식에서는 배열에서 원소를 읽을 때마다 해줘야 한다.

    • 하지만 (EObject가 아닌 한) 배열의 런타임 타입이 컴파일타임 타입과 달라 힙 오염(heap pollution : 아이템 32)을 일으킨다. 힙 오염이 맘에 걸리는 프로그래머는 두 번째 방식을 고수하기도 한다.

제네릭 타입에서의 타입 매개변수

  • 리스트? 배열?

    • 지금까지 설명한 Stack 예는 “배열보다는 리스트를 우선하라”는 아이템 28과 모순돼 보인다.

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

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

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

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

    • Stack<Object>, Stack<int[]>, Stack<List<String>, Stack 등 어떤 참조 타입으로도 Stack을 만들 수 있다. 단,기본 타입은 사용할 수 없다.

    • Stack<int>Stack<double>을 만들려고 하면 컴파일 오류가 난다. 이는 자바 제네릭 타입 시스템의 근본적인 문제이나,박싱된 기본 타입(아이템 61)을 사용해 우회할 수 있다.
  • 타입 매개변수에 제약을 두는 제네릭 타입도 있다. 예컨대 java.util, concurrent.DelayQueue는 다음처럼 선언되어 있다.

    class DelayQueue<E extends Delayed> implements BlockingQueue<E>

    (타입 매개변수 목록인 <E extends Delayed>java.util.concurrent.Delayed의 하위 타입만 받는다는 뜻이다.)

    • 이렇게 하여 DelayQueue 자신과 DelayQueue를 사용하는 클라이언트는 DelayQueue의 원소에서(형변환 없이)곧바로 Delayed 클래스의 메서드를 호출할 수 있다.

    • ClassCastException 걱정은 할 필요가 없다. 이러한 타입 매개변수 E를 한정적 타입 매개변수(bounded type parameter)라 한다.

  • 그리고 하나 더 ! 모든 타입은 자기 자신의 하위 타입 이므로 DelayQueue<Delayed>로도 사용할 수 있음을 기억해두자


핵심 정리

  • 클라이언트에서 직접 형변환해야 하는 타입보다 제네릭 타입이 더 안전하고 쓰기 편하다.

  • 기존 클라이언트에는 아무 영향을 주지 않으면서, 새로운 사용자를 훨씬 편하게 해주는 길이다(아이템 26).

  • 새로운 타입을 설계할 때는 형변환 없이도 사용할 수 있도록 하라.

    ➡ 그렇게 하려면 제네릭 타입으로 만들어야 할 경우가 많다.

    ➡ 기존 타입 중 제네릭이었어야 하는 게 있다면 제네릭 타입으로 변경하자.

0개의 댓글