ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Item 28. 배열보다는 리스트를 사용하라
    Book/Effective Java 3E 2022. 11. 12. 09:10
    반응형

    배열과 제네릭 타입의 차이

    1. 배열은 공변(convariant)이다. Sub가 Super의 하위 타입이라면 배열 Sub[]는 배열 Super[]의 하위 타입이 된다(공변, 즉 함께 변한다는 뜻이다). 반면, 제네릭은 불공변(invariant)이다. 즉, List<Type1>은 List<Type2>의 하위 타입도 아니고 상위 타입도 아니다.
    2. 배열은 실체화(reify)된다. 배열은 런타임에도 자신이 담기로 한 원소의 타입을 인지하고 확인한다(아래 코드 28-1 런타임 실패). 반면, 제네릭은 타입 정보가 런타임에는 소거(erasure)된다. 원소 타입을 컴파일타임에만 검사한다. 소거는 제네릭이 지원되기 전의 레거시 코드와 제네릭 타입을 함께 사용할 수 있게 해주는 메커니즘이다.

     

    // 코드 28-1 런타임에 실패한다.
    Object[] objectArray = new Long[1];
    objectArray[0] = "타입이 달라 넣을 수 없다."; // ArrayStoreException을 던진다.
    
    // 코드 28-2 컴파일되지 않는다.
    List<Object> ol = new ArrayList<Long>(); // 호환되지 않는 타입이다.
    ol.add("타입이 달라 넣을 수 없다.");

      어느 쪽이든 Long용 저장소에 String을 넣을 수는 없습니다. 다만 배열에서는 그 실수를 런타임에야 알게 되지만, 리스트를 사용하면 컴파일할 때 바로 알 수 있습니다.

     

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

     

      생성자에서 컬렉션을 받는 Chooser 클래스를 예로 살펴봅시다. 이 클래스는 컬렉션 안의 원소 중 하나를 무작위로 선택해 반환하는 choose 메서드를 제공하며, 생성자에 어떤 컬렉션을 넘기느냐에 따라 주사위판, 매직 8볼, 몬테카를로(Monte Carlo) 시뮬레이션용 데이터 소스 등으로 사용할 수 있습니다.

    // 코드 28-3 Chooser - 제네릭을 시급히 적용해야 한다!
    public class Chooser {
      private final Object[] choiceArray;
      
      public Chooser(Collection choices) {
        choiceArray = choices.toArray();
      }
      
      public Object choose() {
        Random rnd = ThreadLocalRandom.current();
        return choiceArray[rnd.nextInt(choiceArray.length)];
      }
    }

      이 클래스를 사용하려면 choose 메서드를 호출할 때마다 반환된 Object를 원하는 타입으로 형변환해야 합니다. 혹시나 타입이 다른 원소가 들어 있었다면 런타임에 형변환 오류가 날 것입니다. 이 클래스를 제네릭으로 만들면 다음과 같습니다.

    // 코드 28-4 Chooser를 제네릭으로 변환
    public class Chooser<T> {
      private final T[] choiceArray;
      
      public Chooser(Collection<T> choices) {
        // choiceArray = choices.toArray(); // 컴파일되지 않는다.
        choiceArray = (T[]) choices.toArray(); // warning: [unchecked] unchecked cast
      }
      
      // choose 메서드는 그대로다.
    }

      컴파일 실패를 피해 Object 배열을 T배열로 형변환하면 warning이 발생합니다. T가 무슨 타입인지 알 수 없으니 컴파일러는 이 형변환이 런타임에도 안전한지 보장할 수 없다는 메시지입니다.

     

      비검사 형변환 경고를 제거하려면 배열 대신 리스트를 쓰면 됩니다. 다음 Chooser는 오류나 경고 없이 컴파일됩니다.

    package effectivejava.chapter5.item28;
    
    import java.util.ArrayList;
    import java.util.Collection;
    import java.util.List;
    import java.util.Random;
    import java.util.concurrent.ThreadLocalRandom;
    
    // 코드 28-5 리스트 기반 Chooser - 타입 안전성 확보!
    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()));
        }
    
        public static void main(String[] args) {
            List<Integer> intList = List.of(1, 2, 3, 4, 5, 6);
    
            Chooser<Integer> chooser = new Chooser<>(intList);
    
            for (int i = 0; i < 10; i++) {
                Number choice = chooser.choose();
                System.out.println(choice);
            }
        }
    }

     

    배열과 제네릭에는 매우 다른 타입 규칙이 적용된다.

    배열은 공변이고 실체화되는 반면, 제네릭은 불공변이고 타입 정보가 소거된다.

    그 결과 배열은 런타임에는 타입 안전하지만 컴파일타임에는 그렇지 않다.

    제네릭은 반대다.

    둘을 섞어 쓰다가 컴파일 오류나 경고를 만나면, 가장 먼저 배열을 리스트로 대처하는 방법을 적용해보자.

     

    [참고 정보]

    이펙티브 자바 Effective Java 3/E 도서 [조슈아 블로크 ]

    이펙티브 자바 깃허브 저장소

    <이펙티브 자바, 3판> 번역 용어 해설

    반응형

    댓글

Designed by Tistory.