Post

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

배열과 제네릭의 차이

1) 배열은 공변, 제네릭은 불공변이다.

  • 배열 : Sub가 Super의 하위타입이면 배열 Sub[]는 배열 Super[]의 하위타입이다.
  • 제네릭 : 서로 다른 T1, T2가 있을 때, List<T1>List<T2>의 하위타입도 상위타입도 아니다.
1
2
3
// 배열 : 런타임에 실패한다.
Object[] objectArray = new Long[1];
objectArray[0] = "타입이 달라 넣을 수 없다." // ArrayStoreException을 던진다.
1
2
3
// 제네릭 : 컴파일되지 않는다.
List<Object> o1 = new ArrayList<Long>(); // 호환되지 않는 타입이다.
o1.add("타입이 달라 넣을 수 없다.");

2) 배열은 실체화된다.

  • 배열은 런타임에도 자신이 담기로 한 원소의 타입을 인지하고 확인한다.
  • 반면, 제네릭은 타입 정보가 런타임에는 소거된다.
    • 컴파일 타임에만 원소 타입을 검사한다.
    • 소거는 제네릭 지원 전 레거시 코드와 제네릭 타입을 함께 사용할 수 있게 해주는 메커니즘

배열과 제네릭의 부조화

  • 배열은 제네릭 타입, 매개변수화 타입, 타입 매개변수로 사용할 수 없다.
    • new List<E>[], new List<String>[], new E[]
    • 제네릭 배열 생성 오류
  • 제네릭 배열을 만들지 못하게 막은 이유
    • 타입 안전하지 않기 때문.
    • 컴파일러가 자동 생성한 형변환 코드에서 런타임에 ClassCastException이 발생할 수 있다.
      • 런타임에 해당 예외가 발생하지 않도록 하는 것이 제네릭 타입의 취지이므로 모순이다.

(1)과 같은 제네릭 배열 생성이 가능하다면?

1
2
3
4
5
List<String>[] stringLists = new List<String>[1];  // (1)
List<Integer> intList = List.of(42);               // (2)
Object[] objects = stringLists;                    // (3)
objects[0] = intList;                              // (4)
String s = stringLists[0].get(0);                  // (5)
  • (2)는 원소가 하나인 List<Integer>를 생성한다.
  • (3)은 (1)에서 생성한 List<String>의 배열을 Object 배열에 할당한다
  • (4)는 (2)에서 생성한 List<Integer>의 인스턴스를 Object 배열의 첫 원소로 저장한다.
    • 제네릭은 소거방식으로 구현되어서 런타임시에 List<Integer>의 인스턴스 타입은 단순히 List가 되고, List<Integer>[]의 인스턴스 타입은 List[]가 된다.
    • 따라서 ArrayStoreException이 발생하지 않는다.
  • (5)에서가 문제다.
    • 현재 stringLists의 0번째에는 List<Integer> 인스턴스가 저장돼 있다.
    • 컴파일러는 꺼낸 원소를 자동으로 String으로 형변환 하는데, 이 원소가 Integer 이므로 런타임에 ClassCastException 오류가 발생한다.
    • 이런 일을 방지하려면 제네릭 배열이 생성되지 않도록 (1)에서 컴파일 오류를 내야 한다.

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

  • E, List<E>, List<String> 같은 타입
    • 런타임에 컴파일 타임보다 타입 정보를 적게 가진다.
  • 소거 매커니즘 때문에 List<?>Map<?, ?> 같은 비한정적 와일드카드 타입만 실체화 가능하다.
  • 제네릭 타입과 가변인수 메서드(vargargs method)를 함께 쓰면 경고를 받게 된다.
    • 실체화 불가 타입이기 때문이다.
    • @SafeVarargs 어노테이션으로 대처 가능하다.
  • 배열로 형변환 시, 제네릭 배열 생성 오류나 비검사 형변환 경고가 뜨면 배열인 E[] 대신 컬렉션인 List<E>를 사용하자.
1
2
3
4
5
6
7
8
9
10
11
12
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를 원하는 타입으로 형변환해야한다.
  • 혹시나 타입이 다른 원소가 들어있었다면 런타임에 형변환 오류가 발생한다.

이 클래스를 제네릭 타입으로 변환해보자.

1
2
3
4
5
6
7
8
9
// Chooser를 제네릭으로 만들기 위한 첫 시도 - 컴파일 되지 않는다.
public class Chooser<T> {
    private final T[] choiceArray;
    
    public Chooser(Collection<T> choices) {
        // toArray()를 Object[] 타입으로 형변환 할 수 없어서 컴파일 오류
        choiceArray = choices.toArray();
    }
}
  • toArray()Object[] 타입으로 형변환 할 수 없어서 컴파일 오류가 발생한다.
1
2
3
4
5
6
7
8
// Object 배열을 T 배열로 형변환 하자.
public class Chooser<T> {
    private final T[] choiceArray;
	
    public Chooser(Collection<T> choices) {
        choiceArray = (T[]) choices.toArray(); // 경고
    }
}
  • 제네릭은 런타임에 타입정보가 소거되어 런타임에는 타입을 알 수 없으므로 안전하지 않다.

비검사 형변환 경고를 제거하기 위해서는 배열 대신 리스트를 쓰면 된다.

1
2
3
4
5
6
7
8
9
10
11
12
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));
    }
}

💡 핵심 정리

  • 배열은 공변이고 실체화되지만, 제네릭은 불공변이고 타입 정보가 소거 된다.
  • 그래서 배열은 런타임에 타입 안전하지만, 컴파일 타임에는 그렇지 않다.
  • 제네릭은 반대로 런타임에는 안전하지 않고, 컴파일 타임에는 안전하다.
  • 그래서 둘을 섞어 쓰기는 쉽지 않다. → 배열을 리스트로 대체하자.

참고 🕶️

@SafeVarags

  • SafeVarargs (Java Platform SE 7)
  • 생성자와 메서드의 제네릭 가변인자에 사용할 수 있는 애노테이션
  • 제네릭 가변인자는 근본적으로 타입 안전하지 않다.
    • 가변인자가 배열이므로, 제네릭 배열과 같은 문제가 발생한다.
  • 가변 인자 (배열)의 내부 데이터가 오염될 가능성이 있다.
  • @SafeVarargs를 사용하면 가변 인자에 대한 해당 오염에 대한 경고를 숨길 수있다.
  • 아이템 32. 제네릭과 가변인수를 함께 쓸 때는 신중하라
This post is licensed under CC BY 4.0 by the author.