ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Item 28: Prefer lists to arrays
    독서/Effective Java 2022. 1. 2. 17:18

    배열보다는 리스트를 사용하라

    배열과 제네릭의 첫번째 차이: 공변, 불공변

    Sub가 Super의 하위 타입이라고 가정할 경우

    배열 공변(함께 변함)이기에 Sub[]은 Super[]의 하위 타입이다.

    제네릭은 불공변이기에 List<Sub>은 List<Super>의 하위 타입이 아니다.

    // 테스트를 위한 클래스들이니 Lombok의 Getter, Setter 사용
    @Getter
    @Setter
    public class Parent {
      private int age;
    }
    
    public class Child extends Parent{}
    
    public class NotChild extends Parent{}
    
    @Test
    void runTimeTest(){
      Parent[] array = new Child[1];
      Assertions.assertThatThrownBy(() -> {
        array[0] = new NotChild();
      }).isInstanceOf(ArrayStoreException.class);
    }

    위의 예는 컴파일 단계에서는 에러를 일으키지 않지만, 실행 단계에서 에러를 내뿜는다.

    제너릭으로 생성할 경우 애초에 컴파일 단계에서부터 에러가 발생한다.

    위에서 언급했듯이, List와 ArrayList는 엄연히 다른 타입이고, ArrayList가 List의 하위 타입도 아니기 때문에 생성 시점에서부터 에러가 발생한다.

     

    두번째 차이: 배열은 실체화 된다.

    배열은 런타임에도 자신이 담기로 한 원소의 타입을 인지하고 확인한다.

    앞에서의 runTimeTest의 예를 보면, 실행 시점에 array에 NotChild 타입의 원소를 넣는 순간 타입 체크에서 걸렸기 때문에 ArrayStoreException이 발생했다고 보면 된다.

    반대로 Generic은 타입 정보가 런타임에는 사라지기 때문에(소거), 원소 타입을 컴파일 단계에서만 확인하고 런타임에서는 알 수 없다. 소거는 제너릭 지원 전의 코드와 제네릭 타입을 함께 사용하기 위한 메커니즘이다.

    이러한 차이로 인해 배열 + Generic은 잘 어우러지지 않는다.

    new List<E>[], new List<String>[], new E[]의 방식으로 작성하면 컴파일 단계에서 제네릭 배열 생성 오류가 발생된다.

     

    제네릭 배열을 막은 이유

    제네릭 배열을 막은 이유는 타입 안전하지 않기 때문이다. 이를 허용한다면 컴파일러가 자동 생성한 형변환 코드에서 런타임에 ClassCastException이 발생할 수 있다.

     

    실체화 불가 타입

    E, List, List 같은 타입을 실체화 불가 타입이라 한다. 실체화 되지 않아서 런타임에는 컴파일 타임보다 타입 정보를 적게 가지는 타입이다. 소거 메커니즘 때문에 매개 변수화 타입 가운데 실체화 될 수 있는 타입은 List<?>와 Map같은 비 한정적 와일드 카드 타입뿐이다.

     

    @SelfVarargs

    배열을 제네릭으로 만들 수 없어 귀찮을 때도 있다. 제네릭 컬렉션에서는 자신의 원소 타입을 담은 배열을 반환하는 게 보통은 불가능하다. 또한 제네릭 타입과 가변 인수 메서드를 함께 쓰면 해석하기 어려운 경고 메시지를 받게 된다. 이 문제는 @SafeVarargs 에너테이션(Item 32)으로 대처할 수 있다.

     

    Array -> Generic

    배열로 형변환 할 때 제네릭 배열 생성 오류나 비검사 형변환 경고가 뜨는 경우 대부분은 배열인 E[] 대신 컬렉션인 List를 사용하면 해결된다. 코드가 조금 복잡해지고 성능이 살짝 나빠질 수도 있지만, 그 대신 타입 안전성과 상호 운용성은 좋아진다.

    // Chooser - a class badly in need of generics!
    public class Chooser {
      // 매번 Object 형으로 형변환을 해야한다.
      private final Object[] choiceArray;
      public Chooser(Collection choices) {
        choiceArray = choices.toArray();
      }
      public Object choose() {
        Random rnd = ThreadLocalRandom.current();
        return choiceArray[rnd.nextInt(choiceArray.length)];
      }
    }

    위 클래스는 매번 형 변환해야 하니 리팩토링이 필요하다.

    해당 방식으로 변경하게 될 경우 T를 알 수 없어 컴파일이 불가능하다.

    public class Chooser<T> {
      private final List<T> choiceList;
      public Chooser(Collection<T> choices) {
        choiceList = new ArrayList<>(choices);
      }
      public T choose() {
        Random rnd = ThreadLocalRandom.current();
        return choiceList.get(rnd.nextInt(choiceList.size()));
      }
    }

    이전 예제보다 코드양이 조금 늘었고 아마도 조금 더 느릴 테지만, 런타임에 ClassCastException을 만날 일은 없으니 그만한 가치가 있다.

     

    댓글

Designed by Tistory.