Study/이펙티브 자바 / / 2023. 11. 10. 22:01

[Effective Java 3E] 배열보다는 리스트를 사용하라

💥 개요

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

배열의 경우 Sub가 Super의 하위 타입이라면 배열 Sub[]는 배열 Superp[]의 하위 타입이 된다.(공변)

하지만 제네릭은 불공변이다. List<Sub>가 List<Super>의 하위 타입이 아니다.(불공변)

그럼 제네릭이 더 안 좋은걸까?

 

배열의 문제점

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

위의 코드는 런타임에 실패한다.

하지만 다음 코드는 컴파일 시점에 실패한다.

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

어느쪽이든 문자열을 넣을 수 없지만, 이처럼 컴파일 시점에 알지못해 위험성을 가지게 된다.

 

배열은 실체화된다(reify)

위 코드에서 배열은 Long 배열에 String을 넣으려 하면 ArrayStoreException이 발생한다.

하지만 제네릭은 타입 정보가 런타임에는 소거(erasure)된다. 원소 타입을 컴파일 타임에만 검사하며, 런타임에는 알 수조차 없게 된다는 뜻이다.

소거 : 제네릭이 지원되기 전의 레거시 코드와 제네릭 타입을 함께 사용할 수 있게 해주는 메커니즘

 

둘은 어울릴 수 없다

이 차이로 둘은 잘 어우러지지 못한다. 배열은 제네릭 타입, 매개변수화 타입, 타입 매개변수로 사용할 수 없다.

그 이유는 타입 안전하지 않기 때문이다. 이를 허용하면 컴파일러가 자동 생성한 형변환 코드에서 런타임에 ClassCastException이 발생할 수 있다. 런타임에 ClassCastException이 발생하는 일을 막아주는 취지에 어긋난다.

List<String>[] stringLists = new List<String>[1];
List<Intger> intList = List.of(42);

Obejct[] objects = stringLists;
objects[0] = intList;
String s = stringLists[0].get(0);

line1이 허용된다고 가정하면 2는 원소가 하나인 리스트를 생성한다.

3은 1에서 생성한 List<String> 배열을 Obejct 배열에 할당한다.(배열은 공변이니 아무 문제 없다.)

4는 2에서 생성한 List<Integer>의 인스턴스를 Obejct배열의 첫 원소로 저장한다.(제네릭은 소거 방식이라 문제없다)

즉, 런타임에는 List<Integer> 인스턴스 타입은 단순히 List가 되고, List<Integer>[] 인스턴스 타입은 List[]가 된다.

따라서 4에도 ArrayStoreException이 일어나지 않는다.

 

하지만 List<String>만 담겠다고 선언한 배열에는 지금 List<Integer>가 담겨있다. 

5에서 리스트의 첫 원소를 꺼내려 할 때, 컴파일러는 String으로 형변환을 시도하는데 Integer이므로 ClassCastException이 발생하게 되는 것 이다.

 

실체화 불가 타입

E, List<E>, List<String>과 같은 타입을 실체화 불가 타입이라 한다.

런타임에는 컴파일타임보다 타입 정보를 적게 가지는 타입이다.

소거 매커니즘으로 인해 실체화 될 수 있는 타입은 List<?>와 Map<?,?> 같은 비한정적 와일드카드 타입 뿐이다. 

 

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

제네릭 컬렉션에서는 자신의 원소 타입을 담은 배열을 반환하는게 보통은 불가능하다.

또한 제네릭과 가변 인수 메서드를 쓰면 해석하기 어려운 경고 메시지를 받기도 한다. 이 문제는 @SafeVarargs로 대처가 가능하다.

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)];
    }
}

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

위의 Chooser는 매번 형 변환이 필요하고 문제가 생길 수 있어서 제네릭을 적용해야 한다.

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()));
    }
}

이전보다 코드 양이 늘고, 아마 조금 더 느리지만 런타임에 에러를 만날 일은 없으니 그만한 가치가 있다.

 

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