아이템 28. 배열보다는 리스트를 사용하라. - ksw6169/effective-java GitHub Wiki

배열과 제네릭 타입의 차이

1. 배열은 공변(covariant)이다.

  • 여기서 공변이란 '함께 변한다' 는 뜻이다.
  • Sub가 Super의 하위 타입이라면 배열 Sub[]는 배열 Super[]의 하위 타입이 된다.
  • 반면에 제네릭은 불공변으로 서로 다른 타입 Type1과 Type2가 있을 때 List은 List의 하위 타입도 아니고 상위 타입도 아니다.
  • 다음 코드는 문법상 허용되는 코드지만 런타임에 실패한다.
Object[] objectArray = new Long[1];
objectArray[0] = "타입이 달라 넣을 수 없다."; // ArrayStoreException을 던진다.
  • 하지만 다음 코드는 문법에 맞지 않는다.
List<Object> ol = new ArrayList<Long>();  // 호환되지 않는 타입이다.
o.add("타입이 달라 넣을 수 없다.");
  • 어느 쪽이든 Long용 저장소에 String을 넣을 수 없다. 다만 배열에서는 그 실수를 런타임에야 알게 되지만 리스트를 사용하면 컴파일할 때 바로 알 수 있다.

2. 배열은 실체화(reify)된다.

  • 배열은 런타임에도 자신이 담기로 한 원소의 타입을 인지하고 확인한다. 그래서 Long 배열에 String을 넣으려 하면 ArrayStoreException이 발생한다.
  • 반면, 제네릭은 타입 정보가 런타임에는 소거(erasure) 된다. 원소 타입을 컴파일 타임에만 검사하며 런타임에는 알 수조차 없다는 뜻이다.
  • 이런 주요한 차이로 인해 배열과 제네릭은 잘 어우러지지 못한다.

배열은 제네릭으로 만들 수 없다.

  • 배열은 제네릭 타입, 매개변수화 타입, 타입 매개변수로 사용할 수 없다.
  • 즉, 코드를 new List<E>[], new List<String>[], new E[] 과 같이 작성하면 컴파일 시 제네릭 배열 생성 오류를 일으킨다.

제네릭 배열을 만들지 못하게 막은 이유

  • 타입 안전하지 않기 때문이다.
  • 이를 허용한다면 컴파일러가 자동 생성한 형변환 코드에서 런타임에 ClassCastException이 발생할 수 있다.
  • 런타임에 ClassCastException이 발생하는 일을 막아주겠다는 제네릭 타입 시스템의 취지에 어긋나는 것이다.
  • 다음의 예시처럼 ClassCastException이 발생하는 것을 방지하려면 (1)에서 컴파일 오류를 내야 한다.
// 제네릭 배열을 생성하는 (1)이 허용된다고 가정하자.
(1) List<String>[] stringLists = new List<String>[1];

// (2)는 원소가 하나인 List<Integer>를 생성한다.
(2) List<Integer> intList = List.of(42);

// (3)은 (1)에서 생성한 List<String> 배열을 Object 배열에 할당한다. (배열은 공변이니 문제 X)
(3) Object[] objects = stringLists; 

// (4)는 (2)에서 생성한 List<Integer>의 인스턴스를 Object 배열의 첫 원소로 저장한다. 
// 제네릭은 소거 방식으로 구현되어 이 역시 성공한다.
(4) objects[0] = intList;

// (5) List<String> 인스턴스만 담겠다고 선언한 stringLists 배열에는 지금 List<Integer> 인스턴스가 저장돼있다.
// 이 때 리스트에서 첫 원소를 꺼내려 하면 컴파일러는 꺼낸 원소를 자동으로 String으로 형변환하는데
// 이 원소는 Integer이므로 런타임에 ClassCastException이 발생한다.
(5) String s = stringLists[0].get(0);

실체화 불가 타입(non-reifiable type)

  • 타입 정보가 소거되어 런타임 시점에 구체화되지 않은 타입을 말한다.
  • 예를 들어 List<String>은 컴파일 시점에 제네릭 타입 소거를 수행하면 이후에 List로 인식되므로 런타임 시점에 타입이 실체화되지 않는다.
  • 이러한 소거 메커니즘 때문에 매개변수화 타입 가운데 실체화될 수 있는 타입은 List<?>Map<?,?> 같은 비한정적 와일드카드 타입뿐이다. (Object 타입을 사용하도록 변환된다.)
  • E, List<E>, List<String> 과 같은 타입이 실체화 불가 타입에 해당된다.
  • 배열을 비한정적 와일드카드 타입을 이용해 만들 수는 있지만 유용하게 쓰일 일은 거의 없다.

배열을 제네릭으로 만들 수 없어 귀찮을 때도 있다.

1. 제네릭 컬렉션에서는 자신의 원소 타입을 담은 배열을 반환하는 게 보통은 불가능하다.

2. 제네릭 타입과 가변인수 메소드(varargs method)를 함께 쓰면 해석하기 어려운 경고 메시지를 받는다.

  • 가변인수 메소드를 호출할 때마다 가변인수 매개변수를 담을 배열이 만들어지는데, 이때 그 배열의 원소가 실체화 불가 타입이라면 경고가 발생한다.
  • 이 문제는 @SafeVarargs 로 대처할 수 있다. (이 어노테이션은 단지 사용자가 헷갈리지 않도록 컴파일러 경고를 없애주는 용도로 사용되는데, 실제 제네릭 가변인수 배열이 문제를 일으키는지는 확인을 하고 사용해야 한다.)
// 195p 예제 코드
@SafeVarargs
static <T> List<T> flatten(List<? extends T> ... lists) {
    List<T> result = new ArrayList<>();
    for (List<? extends T> list : lists)
        result.addAll(list);
    return result;
}

3. 제네릭 타입을 배열로 형변환할 때 제네릭 배열 생성 오류나 비검사 형변환 경고가 뜬다.

  • 이 경우 대부분은 배열인 E[] 대신 컬렉션인 List를 사용하면 해결된다.
  • 이로 인해 코드가 조금 복잡해지고 성능 저하가 있을 수 있지만, 타입 안전성과 상호 운용성은 좋아진다.
  • 다음 Chooser 클래스를 사용하려면 choose 메소드를 호출할 때마다 반환된 Object를 원하는 타입으로 형변환해야 한다. 혹시나 다른 타입의 원소가 들어 있었다면 런타임에 형변환 오류가 발생할 것이다.
/**
 * 제네릭 적용이 시급한 Chooser 클래스
 * 클라이언트에서 choose 메소드를 쓸 때마다 형변환이 필요하다.
 */
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)];
    }
}
  • 위 클래스를 제네릭으로 만들어보자.
public class Chooser<T> {
    private final T[] choiceArray;

    public Chooser(Collection<T> choices) {
        choiceArray = (T[]) choices.toArray();
    }

    public T choose() {
        Random rnd = ThreadLocalRandom.current();
        return choiceArray[rnd.nextInt(choiceArray.length)];
    }
}
  • 경고 메시지가 발생한다. T가 무슨 타입인지 알 수 없으니 컴파일러는 이 형변환이 런타임에도 안전한지 보장할 수 없다는 메시지다. (제네릭 타입 소거로 인해 런타임에 무슨 타입인지 확인 불가하다는 의미)

Java Warning "Unchecked Cast"

인자로 들어온 choices는 컴파일 타임에 타입이 소거되어 Collection으로 들어온다.
따라서 이를 T[] 타입으로 형변환하고자 할 때 Collection이 T 타입인지 컴파일러는 알 방법이 없으므로
비검사 형변환 경고를 발생시킨다. (물론 컴파일에는 성공한다.)

참고 자료 : Java Warning "Unchecked Cast"ㅣBaeldung


Chooser.java:9: warning: [unchecked] unchecked cast
    choiceArray = (T[]) choices.toArray();
                                       ^
  required: T[], found: Object[]
  where T is a type-variable:
T extends Object declared in class Chooser
  • 어노테이션을 달아 경고를 숨겨도 되지만 경고의 원인을 제거하는 편이 훨씬 낫다.
  • 위와 같은 비검사 형변환 경고를 제거하려면 배열 대신 리스트를 쓰면 된다.
  • 코드양이 늘고, 속도도 조금 느리지만 런타임에 ClassCastException을 만날 일은 없으니 그만한 가치가 있다.
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()));
    }
}

참고 자료

⚠️ **GitHub.com Fallback** ⚠️