Study/Effective-Java

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

hongeeii 2023. 11. 30. 11:40
728x90
반응형

이왕이면 제네릭 타입으로 만들라

제네릭 타입을 새로 만드는 일은 좀 어려움. => 배워두면 좋음.

// 변경할 예시 코드
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 boolean isEmpty(){ return size == 0;}
  
  private void ensureCapacity(){
    if(elements.length == size)
      elements = Arrays.copyOf(elements, 2 * size + 1);
  }
}

이렇게 하면 꺼내 쓸때 형변환도 해야하고 형변환 시 런타임 오류가 발생할 수도 있음.

제네릭 클래스로 만드는 방법

  • 클래스 선언에 타입 매개변수를 추가한다.

    타입 이름으로는 보통 E를 사용함.
    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[size++] = e;
      }
    
      public E pop(){
        if(size == 0)
          throw new EmptyStackException()'
        E result = elements[--size];
        elements[size] = null; // 다쓴 참조 해제
        return result;
      }
      ... // isEmpty와 ensureCapacity 메서드는 그대로
    }
    
    이상태로 컴파일 하면 아래와 같은 오류가 하나 뜸.
    Stack.java:8: generi arrau creation
      elements = new E[DEFAULT_INITIAL_CAPACITY];
    
    E와 같은 실체화 불가 타입으로는 배열을 만들 수 없기 때문.


    -- 해결책 --
    • Obejct배열을 생선한 다음 제네릭 배열로 형변환(제네릭 배열 생성 금지 제약을 대놓고 우회하는 방법)
      elements = (E[]) enw Object[DEFAULT_INITIAL_CAPACITY];
      
      컴파일러가 오류 대신 경로를 보내긴 하지만 타입 안전하지는 않음.

      컴파일러가 타입 안전을 증명하지 못하니 우리가 직접 확인해야함.

      경고는 @SuppressWarnings 를 사용해 숨김.
      // 배열 elements는 push(E)로 넘어온 E 인스턴스만 담음.=> 따라서 타입 안정성을 보장.
      // 이 배열의 런타임 타임은 E[]가 아닌 Object[]임.
      @SuppressWarnings("unchecked")
      public Stack(){
        elements = (E[]) new Object[DEFAULT_INITIAL_CAPACITY];
      }
      
    • elements 필드의 타입을 E[]에서 Object[]로 바꾸는 것

      이렇게 하면 다른 오류가 뜸
      Stack.javaa:19: incompatible types
      found: Object, required: E
        E result = elements[--size];
      
      반환 하는 원소를 형변환 하면 오류대신 경고가 뜸.
      Stack.java19: warning: [unchecked]unchecked cast
      found: Object, required: E
        E result = (E)elements[--size];
      
      이 경우도 컴파일러가 형변환 안전을 증명할 방법이 없으므로 직접 증명하고 경고를 숨겨주자.
      public E pop(){
        if(size == 0)
          throw new EmptyStackException();
        @SuppressWarnings("unchecked") E result = (E) elements[--size];
        elements[size] = null;
        return result;
      }
      

더 좋은 방법은?

제네릭 배열 생성을 제거하는 두 방법 모두 지지를 받고 있음.

  • 첫번째 방법은 가독성이 더 좋음

    타입 자체를 E[]로 선언하여 E 타입 인스턴스만 받는 것을 어필함.

    코드도 더 짧음.(형변환을 배열 생성시 한번만 해주면 됨.)

    런타임 타입이 컴파일타입과 달라 힙오염(heap pollution)을 일으킴
  • 두번째 방법은 힙오염을 안일으킴.

    형변환을 배열에서 원소를 읽을 때마다 해야함

지금까지 설명한 내용은 "배열보다는 리스트를 우선하라"는 아이템 28과 모순되어 보임.

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

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

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

한정적 타입 매개변수

image
image


제네릭은 컴파일이 되면 소거되고 제네릭 타입은 Object로 치환됨.

이 타입을 한정짓고 싶다면?


한정적 타입 매개변수를 사용하면됨.

image


이렇게만 하면 아래 처럼 오류가 뜸.

Exception in thread "main" java.lang.ClassCastException: class [Ljava.lang.Object; cannot be cast to class [Ljava.lang.Number; ([Ljava.lang.Object; and [Ljava.lang.Number; are in module java.base of loader 'bootstrap')
    at me.whiteship.chapter05.item29.bounded_type.Stack.<init>(Stack.java:21)
    at me.whiteship.chapter05.item29.bounded_type.Stack.main(Stack.java:48)

왜냐하면 한정적 타입 매개변수를 사용하면 Object 의 배열인데
image

매개변수 타입의 배열을 사용하기 위해 캐스팅 하는 과정(E[]로 캐스팅)에서 타입 안전하지 않기때문에 오류를 뱉음.

image

위와 같이 해주면 잘 됨.

image

위와같이 여러 클래스와 인터페이스로 제한하고 있는 매개변수 타입이라면 제한하는 모든것들을 구현하고 있어야함.

인터페이스와 클래스를 둘다 제한에 놓는다면 클래스를 먼저 넣어야함.

728x90
반응형