본문 바로가기

Java

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

 

배열과 제네릭 타입의 차이

1. 배열은 공변(covariant)이다. (함께 변한다는 뜻)

예를들어 Sub가 Super의 하위 타입이라면 배열 Sub[]는 배열 Super[]의 하위 타입이 된다.

반면에 제네릭은 불공변(invariant)이다. List<Sub>는 List<Super>의 하위 타입도 아니고 상위 타입도 되지 않는다.

 

 

아래 코드처럼 배열에서는 코드가 실행되는 런타임 시점에서야 오류가 발생함을 알 수 있지만 리스트의 경우 컴파일 시점에 오류를 확인할 수 있다.

Object[] objectArray = new Long[1];
// ArrayStoreException 발생
objectArray[0] = "Kimtaeng";

// 아예 컴파일 오류
List<Object> objectList = new ArrayList<Long>();
objectList.add("Kimtaeng");

 

 

2. 배열은 실체화(reify) 된다. 

런타임에도 자신이 담기로 한 원소의 타입을 확인함을 뜻한다. 위의 코드에서 ArrayStoreException이 발생한 것도 같은 이유.

하지만 제네릭은 타입 정보가 런타임 시점에 소거된다. 원소 타입을 컴파일 시점에만 검사하기 때문에 런타임 시점에는 알 수 없다.

 

 

왜 제네릭 배열을 생성하지 못할까?

배열은 아래와 같이 제네릭 타입, 매개변수화 타입, 타입 매개변수로 사용할 수 없다.

// 배열은 아래와 같이 사용하면 오류가 발생한다.
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)
  • (3)번: (1)번 과정에서 생성된다고 가정한 제네릭 배열을 Object[]에 할당. 배열은 공변이므로 아무런 문제가 없다.
  • (4)번: (2)에서 생성한 List<Integer>의 인스턴스를 Object 배열의 첫 원소로 저장한다. 제네릭은 런타임 시점에서 타입이 사라지므로 List<Integer>은 List가 되고 List<String>[]는 List[]가 된다. 따라서 ArrayStoreException이 발생하지 않는다.
  • *(5)번: List<String> 인스턴스만 담겠다고 선언한 stringLists 배열에 다른 타입의 인스턴스가 담겨있는데 첫 원소를 꺼내려 한다. 그리고 이를 String으로 형변환하는데, 이 원소는 Integer 타입이므로 런타임에 ClassCastException이 발생한다. 

애초에 제네릭 배열이 생성되지 않도록 (1)에서 컴파일 오류가 발생해야한다!!

 

 

배열로 형변환시 오류가 발생한다면

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

 

choose메서드(컬렉션 안 원소 중 하나를 무작위로 선택해 반환)를 구현해보자.

 

 

[제네릭을 쓰지 않고 구현한 버전]

public class Chooser {
    private final Object[] choiceArray;
    
    public Chooser(Collection choices) { // 생성자에서 Collection을 받는 클래스
        this.choiceArray = choices.toArray();
    }
    
    // 이 메서드를 사용하는 곳에서는 매번 형변환이 필요하다.
    // 형변환 오류의 가능성이 있다.
    public Object choose() {
        Random rnd = ThreadLocalRandom.current();
        return choiceArray[rnd.nextInt(choiceArray.length)];
    }
}

타입이 다른 원소가 들어가 있다면 형변환 오류가 발생한다.

 

 

[E[]로 구현] - 컴파일 되지 않음

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

    public Chooser(Collection<T> choices) {
        // 오류 발생 incompatible types: java.lang.Object[] cannot be converted to T[]
        this.choiceArray = choices.toArray();
    }

    // choose 메소드는 동일하다.
}
// Object 배열을 T 배열로 형변환하면 된다.
this.choiceArray = (T[]) choices.toArray();

컴파일 오류는 사라졌지만 Unchecked Cast 경고가 발생한다. (타입 매개변수 T가 어떤 타입인지 알 수 없으니 형변환이 런타임에도 안전한지 보장할 수가 없다는 메시지)

제네릭은 런타임에는 타입 정보가 소거되므로 무슨 타입인지 알 수 없다. Unchecked Cast과 같은 비검사 형변환 경고를 제거하려면 배열 대신 리스트를 사용하면 된다.

 

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