NMP-Study / EffectiveJava2022

Effective Java Study 2022
5 stars 0 forks source link

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

Closed okhee closed 2 years ago

windowforsun commented 2 years ago

배열과 리스트 차이

배열은 공변, 리스트는 불공변

구분 설명
공변 함께 변한다.
불공변 함께 변하지 않는다.

배열은 공변이기 때문에 아래와 같은 배열 초기화는 가능하다. 배열은 LongObject 의 하위 타입이기 때문에 Long[]Object[] 의 하위 타입임이 성립된다.

Object[] array= new Long[1]; // Ok
array[0] = "런타임 에러 발생";  // java.lang.ArrayStoreException: java.lang.String

하지만 위와 같은 코드는 컴파일 에러를 발생시킬 수 있다.

반면 리스트는 불공변 이기 때문에 아래와 같은 리스트 초기화가는 불가하다. 리스트는 LongObject 의 하위 타입이라도 List<Long>List<Object> 는 상위 혹은 하위 타입의 관계로 바라볼 수 없다.

List<Object> list= new ArrayList<Long>(); // Nope 컴파일 에러 발생

배열, 리스트 모두 Long 타입으로 초기화한 저장소에 String 과 같은 다른 타입을 넣을 수 없다는 점은 동일하지만, 타입 안정성 측면에서 배열은 런타임에 진입해야 알 수 있고, 리스트는 컴파일 타임에 바로 인지 할 수 있다.

배열은 실체화 된다.

배열은 실체화(Reifiable)가 되는데(JLS 4.7), 앞서든 예시 처럼 Long 배열에 String 원소를 넣으면 ArrayStoreException 이 발생한다. 이는 런타임에도 담기로 한 원소의 타입을 인지하고 확인하기 때문이다.

반면 리스트는 타입 정보가 런타임에는 소거된다. (참조1, 참조2) 원소 타입을 컴파일 터임에만 검사하고, 런타임에는 알수 없다는 의미이다. 이는 제네릭 전 레거시 코드와 제네릭 타입을 함께 사용할 수 있게 해주는 메커니즘으로, Java 5 에서 이를 통해 순조로운 전환을 할 수 있었다.

public static <E> void printArray(E[] array) {
    for (E element : array) {
        System.out.printf("%s ", element);
    }
}

// 컴파일 이후
public static void printArray(Object[] array) {
    for (Object element : array) {
        System.out.printf("%s ", element);
    }
}

제네릭 배열은 생성할 수 없다.

위와 같은 이유들로 인해 제네릭배열(new List<E>[], new List<String>[], new E[])은 타입 안전하지 않기 때문에 생성할 수 없는데, 이는 아래와 같은 경우가 발생할 수 있기 때문이다.

List<String>[] stringLists = new List<String>[1]; // (1) 허용된다고 가정해보자.
List<Integer> intList = List.of(42);              // (2) 원소가 하나인 List<Integer> 생성
Object[] objects = stringLists;                   // (3) stringLists를 objects에 할당
objects[0] = intList;                             // (4) intList를 objects의 첫번째 원소로 저장한다.
String s = stringLists[0].get(0);                 // (5) stringList[0]에 들어가있는 첫번째 요소는 Integer이므로 ClassCastException오류 발생.

컴파일러가 자동 생성한 형변환 코드에서 런타임 시점에 ClassCastException 이 발생할 수 있기 때문이다. 이는 ClassCastException 발생을 막겠다는 제네릭 타입의 취지와도 어긋나는 일이기도 하다. 이러한 이유로 (1) 시점에서 부터 컴파일 오류를 발생시킨다.

실체화 불가 타입

배열은 실체화가 가능하지만, 제네릭은 특별한 경우를 제외하면 실체화 불가(Non-Reifiable Types)하다.

종류 설명 예시
실체화 타입 런타임에도 타입 정보를 가지고 활용한다. String[], Integer[], List<?>, Map<?, ?> ...
실체화 불가 타입 런타임에 타입 정보를 가지지 않고, 컴파일 타임에 소거된다. E, List<E>, List<String>, ...

제네릭은 타입 소거로 인해 실체화 되지 않아서 런타임에는 컴파일타임보다 타입 정보를 적게 가지는 타입을 의미한다. 예외가 있다면 비한정 와일드 카드 타입(#26 ) 이다.

배열보다는 리스트를 사용하면 된다.

배열로 형변환할때 제네릭 배열 생성 오류나 비검사 형변환 경고가 뜨는 경우 대부분 배열인 E[] 대신, List<E> 를 사용하면 해결 가능하다.

public class Chooser {
    private final Object[] choiceArray;

    public Chooser(Collection choices) {
        this.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) {
        // 오류 발생 incompatible types: java.lang.Object[] cannot be converted to T[]
        // T[] 로 형변환 필요
        // 하지만 여전히 Unchecked cast 경고는 발생
        this.choiceArray = (T[]) choices.toArray();
    }

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

choose() 메소드 사용시 형변환에 대한 안전성은 확보 했지만, 생성자에서 Unchecked Cast 경고가 발생하고 있다. 타입 매개변수 T 가 어떤 타입인지 알 수 없기 때문에 형변환이 이뤄지는 런타임에 타입 안전성을 보장할 수 없다는 의미이다. (제네릭은 런타임에 타입 정보가 소거되기 때문) Unchecked Cast 경고는 배열을 리스트로 변경해주면 간단하게 해결 할 수 있다.

public class Chooser<T> {
    private final List<T> choiceList;

    public Chooser(Collection<T> choices) {
        this.choiceList = new ArrayList<>(choices);
    }

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

배열 대신 리스트를 사용함으로써 런타임에 ClassCastException 을 만날일이 없어져서 타입 안전성이 보장된다.

종류 특징
배열 - 공변
- 실체화
- 런타임시 타입 안전
제네릭 - 불공변
- 타입 정보 소거
- 컴파일 타임 타입 안전

컴파일 타임에 타입 검증을 수행하는 제네릭을 사용해서 런타임에 타입 예외를 차단 할 수 있기 때문에 약간의 성능 감소가 있더라도 배열보다는 리스트를 사용하자.