1 minute read

item 28 배열보다는 리스트를 사용하라.

배열과 제너릭

배열은 공변이고, 제너릭은 불공변이다.

배열(공변)은 상위 타입(Super)과 하위 타입(Sub)의 상속 관계가 유지되고, 제너릭(불공변)은 그렇지 않다.

	Object[]objectArray=new Long[1];
    objectArray[0]="타입이 달리 넣을 수 없다."; 
    // 위 코드는 문법상 허용되지만,
    // 런타임에 ArrayStoreException을 던진다.
	List<Object> ol=new ArrayList<Long>(); 
    ol.add("타입이 달라 넣을 수 없다.");
    // 위 코드는 호환되지 않는 타입으로, 
    // 문법에 맞지 않아 컴파일 단계에서 오류가 발생한다.

배열은 실체화(reify)되고, 제네릭은 소거(erasure)된다.

  • 배열은 런타임 시점에 담기로 한 원소의 타입을 인지한다.
  • 제네릭은 타입 정보를 컴파일 시점에 검사하고, 런타임에는 소거한다.

이렇듯 배열과 제네릭은 주요한 차이가 있기에, 이 둘을 함께 사용하지 쉽지 않다.

실체화 불가 타입

  • E, List<E>, List<String> 같은 타입을 실체화 불가 타입(non-reifiable type) 이라 한다.
  • 실체화되지 않기 때문에 런타임에는 컴파일타임보다 타입 정보를 적게 가진다.

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

  • 제네릭은 자신의 원소 타입을 담은 배열을 반환하는 것이 보통 불가능하다.
  • 제네릭과 가변인수 메서드를 함께 사용하면 난해한 경고 메시지가 발생할 것이다. -> 이는 @SafeVarargs 로 대처가 가능하다.

배열로 형변환 할때 오류 혹은 경고가 뜨는 경우, 배열E[] 대신 리스트List<E>를 사용하라.

코드가 복잡해지고 성능이 나빠질 수 있지만, 타입 안정성과 상호운용성이 좋아진다.

import java.util.concurrent.ThreadLocalRandom;

// 제네릭을 사용하지 않은 예제
public class Chooser {
    private final Object[] choiceArray;

    public Chooser(Collection choices) {
        choiceArray = choices.toArray();
    }

    public Object choose() { 
    // 메서드를 호출할 때마다, 
    // 반환된 Object를 원하는 타입으로 형변환해야 한다.
        Random rnd = ThreadLocalRandom.current();
        return choiceArray[rnd.nextInt(choiceArray.length)];
    }
}

// 제네릭으로 리팩토링하였으나, 비검사 형변환 경고가 발생하는 예제.
// 다음 코드는 동작하지만 컴파일러가 안전을 보장하지 않는다.
public class Chooser<T> {
    private final T[] choiceArray;

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

// 배열 대신 리스트를 사용한 예제
// 타입 안정성이 보장된다.
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));
    }
}

Leave a comment