Posts [이펙티브자바] 아이템28-배열보다는 리스트를 사용하라
Post
Cancel

[이펙티브자바] 아이템28-배열보다는 리스트를 사용하라

배열과 제네릭 타입의 중요한 두 가지 차이

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

  • 어려워보이지만 뜻은 간단하다.
  • Sub가 Super의 하위 타입이라면 배열 Sub[]는 배열 Super[]의 하위 타입이 된다.(공변, 즉 함께 변한다는 뜻이다)
  • 반면 제네릭은 불공변(invariant)이다. 즉, 서로 다른 타입 Type1과 Type2가 있을때, List<Type1>List<Type2>의 하위 타입도 아니고 상위 타입도 아니다.
  • 사실 문제가 있는건 배열쪽이다. 아래 코드는 문법상 허용되는 코드다.
1
2
Object[] objectArray = new Long[1];
objectArray[0] = "타입이 달라 넣을 수 없다."; // ArrayStoreException을 던진다.
  • 위의 코드는 런타임에 ArrayStoreException 이 발생하여 실패한다.
1
2
List<Object> ol = new ArrayList<Long>();  // 호환되지 않는 타입이다.
o1.add("타입이 달라 넣을 수 없다.");
  • 하지만 위의 제네릭 타입의 경우엔 컴파일 조차 되지 않는다.
  • 어느 쪽이든 Long용 저장소에 String을 넣을 순 없다.
    • 다만 배열에선 그 실수를 런타임에야 알게 되지만, 리스트를 사용하면 컴파일시 바로 알 수 있다.

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

  • 제네릭은 타입 정보가 런타임에는 소거(erasure)된다. 원소 타입을 컴파일타임에만 검사하며 런타임에는 알 수조차 없다는 뜻이다.
    • 소거는 제네릭이 지원되기 전의 레거시 코드와 제네릭 타입을 함께 사용할 수 있게 해주는 메커니즘이다.

위와 같은 차이로 인해 배열과 제네릭은 잘 어우러지지 못한다!

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

제네릭 배열을 막은 이유는 타입 안전하지 않기 때문이다.

  • 이를 허용한다면 컴파일러가 자동 생성한 형변환 코드에서 런타임에 ClassCastException이 발생할 수 있다. 런타임에 ClassCastException이 발생하는 일을 막아주겠다는 제네릭 타입 시스템의 취지에 어긋나는 것이다.
  • 예제 코드 28-3을 참고하자.

E, List, List 같은 타입을 실체화 불가 타입(non-reifiable type)이라 한다.

  • 실체화되지 않아서 런타임에는 컴파일타임보다 타입 정보를 적게 가지는 타입이다.

Chooser 클래스 예제

  • 생성자에서 컬렉션을 받는 Chooser 클래스를 예로 살펴보자.
    • 이 클래스는 컬렉션 안의 원소 중 하나를 무작위로 선택해 반환하는 choose() 메서드를 제공한다.
    • 생성자 안의 어떤 컬렉션을 넘기느냐에 따라 주사위판, 매직 8볼, 몬테카를로 시물레이션용 데이터 소스 등으로 사용할 수 있다.
  • 아래 예제 코드는 제네릭을 쓰지 않고 구현한 가장 간단한 버전이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
// 코드 28-4 Chooser - 제네릭을 시급히 적용해야 한다.
public class Chooser<T> {
    private final Object[] choiceArray;

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

    public T choose() {
        Random rnd = ThreadLocalRandom.current();
        return choiceArray[rnd.nextInt(choiceArray.length)];
    }
}
  • 이 클래스를 사용하려면 choose 메서드를 호출할 때마다 반환된 Object를 원하는 타입으로 형변환해야 한다.
  • 혹시나 타입이 다른 원소가 들어 있었다면 런타임에 형변환 오류가 날 것이다.
  • 이를 제네릭으로 만들어보자.
1
2
3
4
5
6
7
8
9
public class Chooser<T> {
    private final T[] choiceArray;
    
    public Chooser(Collection<T> choices) {
        choiceArray = choices.toArray();
    }
    
    // choose 메서드는 그대로
}
  • 이 클래스를 컴파일하면 아래와 같은 오류 메시지가 출력될 것이다.
1
2
3
4
5
6
Chooser.java:9: error: incompatible types: Object[] cannot be
converted to T[]
        choiceArray = choices.toArray();
                                     ^
    where T is a type-variable:
      T extends Object declared in class Chooser
  • 걱정할 거 없다. Object 배열을 T로 형변환하면 된다.
1
choiceArray = (T[]) choices.toArray();

그런데 이번엔 경고가 뜬다.

1
2
3
4
5
6
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
  • T가 무슨 타입인지 알 수 없으니 컴파일러는 이 형변환이 런타임에도 안전한지 보장할 수 없다는 메시지다.
  • 제네릭에서는 원소의 타입 정보가 소거되어 런타임에는 무슨 타입인지 알 수 없음을 기억하자!
  • 위 코드는 정상적으로 동작하지만 컴파일러가 안전을 보장하지 못한다.
  • 코드를 작성하는 사람이 안전하다고 확신한다면 주석을 남기고 애너테이션을 달아 경고를 숨겨도 된다.
  • 하지만 애초에 경고의 원인을 제거하는 것이다 낫다! 비검사 형변환 경고를 제거하려면 배열 대신 리스트를 쓰면 된다.
  • 다음 Chooser는 오류나 경고 없이 컴파일된다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 코드 28-6 리스트 기반 Chooser - 타입 안전성 확보! (168쪽)
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);
        }
    }
}
  • 이번 버전은 코드양이 조금 늘었고 아마도 족므 더 느릴테지만, 런타임에 ClassCastException을 만날 일은 없으니 그만한 가치가 있다.

핵심 정리: 배열과 제네릭은 매우 다른 타입 규칙이 적용된다. 배열은 공변이고 실체화되는 반면, 제네릭은 불공변이고 타입 정보가 소거된다. 그 결과 배열은 런타임에는 타입 안전하지만 컴파일타임에는 그렇지 않다. 제네릭은 반대다. 그래서 둘을 섞어 쓰기란 쉽지 않다. 둘을 섞어 쓰다가 컴파일 오류나 경고를 만나면, 가장 먼저 배열을 리스트로 대체하는 방법을 적용해보자.

출처 및 참고

This post is licensed under CC BY 4.0 by the author.

[이펙티브자바] 아이템27-비검사 경고를 제거하라

[이펙티브자바] 아이템29-이왕이면 제네릭 타입으로 만들라