Book/Effective Java 3E

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

hanseom 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판> 번역 용어 해설

반응형