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

Effective Java 3e 아이템 28를 요약한 내용 입니다.

Effective Java 3e 아이템 28를 요약한 내용 입니다.

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

배열은 공변이다.

SubSuper의 하위 타입이라면 배열 Sub[]는 배열 Super[]하위 타입이 된다. 반면 제네릭은 불공변이다. 이것만 보면 제네릭에 문제가 있다고 생각할 수도 있지만 사실 문제가 있는 건 배열 쪽이다.

Object[] objectArray = new Long[1];
objectArray[0] = "타입이 달라 넣을 수 없다."; // ArrayStoreException을 던진다.

다음 코드도 살펴 보자

List<Object> ol = new ArrayList<Long>(); // 호환되지 않는 타입이다.
ol.add("타입이 달라 넣을 수 없다.");

어느 쪽이든 Long용 저장소에 String을 넣을 수는 없다. 다만 배열 에서는 그 실수를 런타임에야 알게 되지만, 리스트를 사용하면 컴파일할 때 바로 알 수 있다.

배열은 실체화가 된다.

배열은 런타임에도 자신이 담기로 한 원소의 타입을 인지하고 확인한다. 그래서 이전 예제에서 보듯이 Long 배열에 String을 넣으려 하면 ArrayStoreException이 발생한다. 반면, 앞서 이야기했듯 제네릭은 타입 정보가 런타임에는 소거된다.

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

제네릭 배열을 만들지 못하게 막은 이유는 무엇일까?

타입 안전하지 않기 때문이다. 이를 허용한다면 컴파일러가 자동 생성한 형변환 코드에서 런타임에 ClassCastException이 발생할 수 있다.

E, List<E>, List<String> 같은 타입을 실체화 불가 타입이라 한다. 실체화 되지 않아서 런타임에는 컴파일 타임보다 타입 정보를 적게 가지는 타입이다. 소거 메커니즘 때문에 매개 변수화 타입 가운데 실체화 될 수 있는 타입은 List<?>Map<?,?>같은 비 한정적 와일드 카드 타입뿐이다.

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

**제네릭 컬렉션에서는 자신의 원소 타입을 담은 배열을 반환하는 게 보통은 불가능하다.** 또한 제네릭 타입과 가변 인수 메서드를 함께 쓰면 해석하기 어려운 경고 메시지를 받게 된다. 이 문제는 @SafeVarargs 에너테이션으로 대처할 수 있다.

@SafeVarargs는 구체적으로 어떻게 사용할 수 있을까? 제네릭을 가변 매개변수로 사용한 경우 잠재적 에러에 대한 경고를 무시할 수 있다. @SuppressWarning를 이용하여 제네릭 매개변수에 대한 경고 무시를 대체하기 위해 나옴

public class JavaExample{  
    @SafeVarargs
    private void print(List... names) {  
        for (List<String> name : names) {  
            System.out.println(name);  
        }  
    }  
    public static void main(String[] args) {  
        JavaExample obj = new JavaExample();  
        List<String> list = new ArrayList<String>();  
        list.add("Kevin");  
        list.add("Rick"); 
        list.add("Negan");
        obj.print(list);  
    }      
}

배열로 형변환 할 때 제네릭 배열 생성 오류나 비검사 형변환 경고가 뜨는 경우 대부분은 배열인 E[] 대신 컬렉션인 List<E>를 사용하면 해결된다. 코드가 조금 복잡해지고 성능이 살짝 나빠질 수도 있지만, 그 대신 타입 안전성과 상호 운용성은 좋아진다.

public class Chooser {
    private final Object[] choiceArray;

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

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

위 클래스는 매번 형 변환해야 하니 리팩토링이 필요하다.

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

    public Chooser(Collection choices) {
    choiceList = new ArrayList<>(choises);
    }

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

이전 예제보다 코드양이 조금 늘었고 아마도 조금 더 느릴 테지만, 런타임에 ClassCastException을 만날 일은 없으니 그만한 가치가 있다.

정리

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

출처

Last updated