Effective Java

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

로드태환 2022. 3. 2. 13:00

배열과 제네릭 타입은 크게 두가지가 다르다

 

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

Sub 가 Super의 하위 타입이면 Sub[]는 Super[]의 하위 타입이지만 List<Sub>는 List<Super>의 하위 타입이 아니다.
이러한 차이는 아주 큰 문제가 된다.

Object[] objectArray = new Long[1];
objectArray[0] = "안들어가쥬";

List<Object> ol = new ArrayList<Long>();
ol.add("컴파일도 안됨");

위에서 배열의 경우는 컴파일은 되지만 런타임에서 ArrayStoreException을 던진다.

반면 리스트의 경우는 컴파일조차 안된다. 즉 런타임 Exception을 걱정하지 않아도 된다.

 

2. 배열은 실체화 되지만 제네릭은 되지 않는다.

 배열은 런타임에도 자신이 담기로한 원소 타입을 기억하고 있지만 제네릭은  원소 타입을 컴파일 타임에만 고려하고 런타임에는 소거된다.

소거는 제네릭 이전의 레거시와의 호환성 문제를 해결해주는 메커니즘이다. 

 

위 차이들 때문에 배열과 제네릭은 잘 어울리지 못하고 함께 쓰는것이 허용 된다면 타입 안정성 때문에 CastException이 발생할 수 있다.

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

(1)이 허용된다면 (4)까지는 문제 없이 컴파일 되고 런타임에도 문제 없이 돌아간다. 소거방식 덕분에 런타임에는 List<Integer>는 단순히 List가 되기 때문이다. 하지만 이후 stringList 의 첫 리스트에서 첫 원소를 꺼낼 때 컴파일러가 원소를 String 타빕으로 형변환 하는데 해당 원소는 Integer이기 때문에 ClassCastException이 발생한다.

이러한 런타임 예외를 막으려면 제네릭 배열이 생성되지 않도록 컴파일 오류를 내야한다.

 

E, List<E>, List<String> 같은 타입은 실체화 되지 않아 런타임에 컴파일타임보다 정보를 적게 가지는데 이를 실체화 불가 타입이라 한다.

반면 List<?>같은 비한정적 와일드카드 타입은 실체화가 가능해 배열로 만들수 있지만 유용하지는 않다. 때문에 제네릭 컬렉션은 자신의 원소타입을 담은 배열을 반환하는게 보통 불가능하다.

또한 가변인수 메서드와 함께 쓰면 어려운 경고 메세지를 받기도 한다. 가변인수 메서드는 호출될 때 가변인수 인자를 담을 배열을 하나 만들기 때문이다. 이는 뒤에 나올 @Safevarages 로 해결 가능하다.

 

배열로 형변환 할 때 제네릭 배열생성이나 비검사 경고가 뜨는 경우는 배열대신 List<E>를 사용하면 대부분 해결된다. 코드가 복잡해지고 성능이 조금 느려질 수 는 있지만 타입 안전성과 호환성이 좋아져 그럴만한 가치가 있다.

 

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

Chooser는 생성자에서 받은 컬랙션안의 원소 중 하나를 랜던하게 넘기는 클래스이다.  클래스 외부에서 choose 메서드를 사용하려면 반환된 Object 를 원하는 타입으로 형 변환 해야한다. 만약 타입이 다른 원소가 있으면 런타임에 오류가 난다.

위의 클래스를 제네릭 타입으로 바꾼다면 컴파일러가 런타임에도 안전하다는 보장을 못한다는 경고를 내보내지만 작성하는 사람이 확신한다면 애너테이션을 달고 주석을 남겨 경고를 숨겨도 무방하다.

하지만 경고의 원인을 없애는 편이 좋으므로 배열을 List로 바꾸어 주면 더 간단하고 더 안전하다.

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