아이템 28. 배열보다는 리스트를 사용하라

문법식·2022년 8월 9일
0

Effective Java 3/E

목록 보기
28/52

배열과 제너릭 타입에는 중요한 차이 두 가지가 있다.

  • 배열은 공변이지만 제너릭 타입은 불공변이다.
  • 배열은 실체화되어 런타임에도 자신이 담기로 한 원소의 타입을 인지하고 확인한다. 제너릭은 타입 정보가 런타임에는 소거된다.

배열은 공변, 제너릭 타입은 불공변

SubSuper의 하위 타입이라면 배열 Sub[]는 배열 Super[]의 하위 타입이 된다.(공변). 반면, 제너릭은 불공변이다. 즉, 서로 다른타입 Type1Type2가 있을 때 List<Type1>List<Type2>의 하위 타입도 아니고 상위 타입도 아니다.
배열의 문제는 다음 코드를 보면서 알아보자

Object[] objectArray = new Long[1];
objectArray[0] = "문자열";

위 코드는 문법적으로 허용되지만, Long용 저장소에 String을 넣을 수는 없기 때문에 런타임에서 오류가 발생한다.

List<Object> ol = new ArrayList<Long>();
ol.add("문자열");

위 코드는 문법적으로 허용되지 않는다. 마찬가지로 Long용 저장소에 String을 넣을 수 없다. 하지만 리스트를 사용하면 문법적으로 허용되지 않아 배열과 다르게 컴파일할 때 바로 알 수 있다.


배열 실체화, 제너릭 타입 소거

위에서 언급했듯이 배열은 런타임에도 자신이 담기로 한 원소의 타입을 인지하고 확인한다. 반면, 제너릭은 타입 정보를 컴파일타임에만 검사하고 런타임에는 소거된다.

위의 중요한 두 차이로 인해 배열과 제너릭은 잘 어우러지지 못한다. 예컨대 배열은 제너릭 타입, 매개변수화 타입, 타입 매개변수로 사용할 수 없다. 즉, 코드를 new List<E>[], new List<String>[], new E[]식으로 작성하면 컴파일할 때 제너릭 배열 생성 오류를 일으킨다.

제네릭 배열을 만들지 못하게 하는 이유는 타입이 안전하지 않기 떄문이다. 이를 허용하면 컴파일러가 자동 생성한 형변환 코드에서 런타임에 ClassCastException 이 발생할 수 있다. 제너릭 타입 시스템의 취지에 어긋나는 오류가 발생한 것이다.

다음 코드로 구체적인 상황을 살펴본다.

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)

제너릭 배열을 생성하는 (1)이 허용된다고 가정한다. (2)는 원소가 하나인 List<Integer>를 생성한다. (3)은 (1)에서 생성한 List<String>의 배열을 Objects 배열에 할당한다. 배열은 공변이니 아무 문제가 없다. (4)는 (2)에서 생성한 List<Integer>의 인스턴스를 Object 배열의 첫 원소로 저장한다. 제너릭은 소거 방식으로 구현되어서 이 역시 문제가 없다. 이제부터가 진짜 문제다. List<String> 인스턴스만 담겠다고 선언한 stringLists배열에는 List<Integer> 인스턴스가 저장돼 있다. 그리고 (5)는 이 배열의 처음 리스트에서 첫 원소를 꺼내려 한다. 컴파일러는 꺼낸 원소를 자동으로 String 형변환하는데, 이 원소는 Integer이므로 런타임에 ClassCastException이 발생한다. 이런 일을 방지하려면 제네릭 배열이 생성되지 않도록 (1)에서 컴파일 오류를 내야 한다.


배열로 형변환할 때 제너릭 배열 생성 오류나 비검사 형변환 경고가 뜨는 경우 대부분 배열인 E[] 대신 컬렉션인 List<E>를 사용하면 해결된다.
생성자에서 컬렉션을 받는 Chooser 클래스를 예로 살펴본다.

public class Chooser {
    private final Object[] choiceArray;

    public Chooser(Collection choices) {
        choiceList = choices.toArray();
    }

    public T choose() {
        Random rnd = ThreadLocalRandom.current();
        return choiceList.get(rnd.nextInt(choiceArray.length));
    }
}    

이 클래스를 사용하려면 choose 메서드를 호출할 때마다 반환된 Object를 원하는 타입으로 형변환해야 한다. 타입이 다른 원소가 들어 있다면 런타임에 형변환 오류가 날 것이다. 다음과 같이 클래스를 제너릭으로 수정해본다.

public class Chooser<T> {
    private final T[] choiceArray;

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

    ...
}    

컴파일하면 Object[]T[]로 변환될 수 없다고 오류가 날 것이다. 아래와 같이 Object 배열을 T배열로 형변환하면 된다.

public class Chooser<T> {
    private final T[] choiceArray;

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

    ...
}    

이번엔 비검사 형변환 경고가 뜬다.
T가 무슨 타입인지 알 수 없으니 컴파일러는 이 형변환이 런타임에도 안전한지 보장할 수 ㅇ벗다는 메시지다. 제너릭에서는 원소의 타입 정보가 소거되어 런타임에는 무슨 타입인지 알 수 없음을 기억해야 한다. 코드를 작성한 사람이 안전하다고 확신한다면 주석을 남기고 경고를 숨겨도 된다. 하지만 애초에 경고의 원인을 제거하는 편이 훨씬 낫다.
비검사 형변환 경고를 제거하려면 배열 대신 리스트를 쓰면 된다. 다음과 같이 코드를 작성하면 오류나 경고 없이 컴파일 된다.

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()));
    }
}
profile
백엔드

0개의 댓글