JAVA

[Effective Java] Item 28. 배열보다는 리스트를 사용하라

혀내 2023. 7. 4. 14:40
반응형

배열 ↔ 제네릭

1. 배열은 공변, 제네릭은 불공변이다.

배열은 공변이다.

즉, Sub 클래스가 Super 클래스의 하위 타입이면, 배열 Sub[]은 배열 Super[]의 하위 타입이 된다.

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

그러므로 이 코드는 컴파일은 성공하지만 런타임 시점에 실패한다.

 

 

제네릭은 불공변이다.

서로 다른 타입 Type1과 Type2이 있을 때, List<Type1>과 List<Type2>는 상위 타입도, 하위 타입도 아니다.

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

이 코드는 컴파일이 아예 되지 않는다. 보통은 런타임보다 컴파일 시점에 에러를 알아채는 것이 훨씬 편할 것이다.

 

 

 

 

2. 배열은 실체화(reify)되지만, 제네릭은 소거된다.

배열은 실체화된다.

- 런타임에도 자신이 담기로 한 원소의 타입을 인지하고 확인한다.

- 위 코드처럼 Long 배열에 String을 넣으려고 하면 ArrayStoreException이 발생한다.

 

 

제네릭은 소거된다.

- 제네릭은 타입 정보가 런타임에는 소거된다.

- 원소 타입을 컴파일 타임에만 검사하며 런타임에는 알 수조차 없다.

 

 

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

 

 

 

제네릭 배열 생성을 막은 이유

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

 

제네릭 타입의 배열 생성이 가능하다고 가정하고 다음 코드를 보자.

List<String>[] stringLists = new List<String>[1];
List<Integer> intList = List.of(42);				
Object[] objects = stringLists;						
objects[0] = intList;								
String s = stringLists[0].get(0);

마지막 줄에서 String 변수에 Integer이 들어가면서 ClassdCastException이 발생하고 만다.

 

 

 

실체화 불가 타입

- E, List<E>, List<String>와 같은 타입

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

- 매개변수화 타입 가운데 실체화될 수 있는 타입: List<?>와 Map<?, ?> 같은 비한정적 와일드카드 타입

 

 

 

 

배열을 제네릭으로 만들 수 없어 귀찮은 경우

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

2. 제네릭 타입과 가변인수 메서드를 함께 쓰면 해석하기 어려운 경고 메시지를 받는다.

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

 

 예로 생성자에서 컬렉션을 받는 Chooser 클래스를 살펴보자. Chooser 클래스는 컬렉션 안의 원소 중 하나를 무작위로 선택해 반환하는 choose 메서드를 제공한다.

 

public class Chooser {
    private final Object[] choiceArray;
    
    public Chooser(Collection choices) {
        choiceArray = choices.toArray();
    }
    
    public Object choose() {
        Random rnd = ThreadLocalRandom.current();
        return choiceArray[rnd.nextInt(choiceArray.length)];
    ]

 

 

 이 클래스를 사용하려면 choose 메서드를 호출할 때마다 반환된 Object를 원하는 타입으로 형변환해야 한다. 그러므로 이 클래스를 제네릭으로 만들어보자.

 

 

public class Chooser<T> {
    private final T[] choiceArray;
    
    public Chooser(Collection<T> choices) {
        choiceArray = (T[]) choices.toArray();
    }
    
    // chose 메서드는 그대로다.
}

 

이 코드는 실행은 되나, 아래 코드에서 경고가 뜬다.

choiceArray = (T[]) choice.toArray();

 

 T가 무슨 타입인지 알 수 없으니 컴파일러는 이 형변환이 런타임에도 안전한지 보장할 수 없다는 메시지가 뜬다. 이 비검사 형변환 경고는 배열 대신 리스트를 쓰면 안전하게 제거할 수 있다. @SafeVarargs 어노테이션을 이용해 코드가 안전하다고 표시하는 방식으로도 제거할 수는 있다. 

 

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

 

 이제 오류나 경고 없이도 컴파일이 된다. 이전보다 코드양이 조금 늘고 조금 더 느리지만, 런타임에 ClassCastException을 만날 일이 없어진다.

 

 

 

 

참고 자료:

 

이펙티브 자바 Effective Java 3/E - YES24

자바 플랫폼 모범 사례 완벽 가이드 - Java 7, 8, 9 대응자바 6 출시 직후 출간된 『이펙티브 자바 2판』 이후로 자바는 커다란 변화를 겪었다. 그래서 졸트상에 빛나는 이 책도 자바 언어와 라이브

www.yes24.com

 

반응형