Effective Java - 제네릭(2)

SeungHyuk Shin·2021년 10월 7일
0

Effective Java

목록 보기
15/26
post-thumbnail

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


첫 번째로 배열은 공변(covariant)입니다. 예를들어 SubSuper의 하위 타입이라면 배열 Sub[]는 배열 Super[]의 하위 타입이 된다고 할 수 있다. 이를 공변이라고 한다. 즉 함께 변한다는 뜻이다.

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

단순 비교만으로는 제네릭에 문제가 있다고 생각하지만, 사실 문제가 있는 건 배열이다. Long 타입용 저장소에 String 타입을 넣을 수는 없다. 아래의 코드처럼 배열에서는 코드가 실행되는 런타임 시점에서야 오류가 발생함을 알 수 있지만 리스트의 경우 컴파일 시점에 오류를 확인할 수 있다.

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

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

배열은 공변으로 존재하고, 제네릭은 불공변으로 존재한다.

E, List<E>, List<String> 같은 타입을 실체화 불가 타입(non-reifiable type)이라 한다. 제네릭 소거로 인해 실체화되지 않아서 런타임 시점에 컴파일타임보다 타입 정보를 적게 가지는 타입을 말한다.

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

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[]
        this.choiceArray = choices.toArray();
    }

    // choose 메소드는 동일하다.
}

incompatible types 오류는 아래와 같이 코드를 변경하면 해결된다.

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

정리해보면 배열은 공변이고 실체화되는 반면, 제네릭은 불공변이고 타입 정보가 소거된다. 따라서 배열은 런타임에는 타입 안전하지만 컴파일타임에는 안전하지 않다. 제네릭은 그 반대로 적용된다.

0개의 댓글