[ Item28 ] 배열보다는 리스트를 사용하라

둥그냥·2022년 4월 24일
0

Effective Java 독서

목록 보기
5/15

📚 배열보다는 리스트를 사용하라

배열과 제네릭의 차이

1. 공변 vs 불공변

  • 배열은 공변(covariant)이다.
    • 배열 Sub[]은 배열 Super[]의 하위 타입이다 (공변 -> 함께 변하다)
  • 제네릭은 불공변이다.
    • 서로 다른 타입 Type1과 Type2가 있으면 List은 List의 상위 타입도, 하위 타입도 아니다.

2. 실체화 vs 소거

  • 배열은 실체화(reify)된다.
    • 배열은 런타임에도 자신이 담기로 한 원소의 타입을 인지하고 확인힌다.
  • 제네릭은 런타임 때 타입 정보가 소거(erasure)된다.
    • 원소 타입을 컴파일타임에만 검사하며 런타임에는 알 수가 없다.

제네릭 배열을 만들지 못하게 막은 이유

위의 주요 차이로 인해 배열과 제네릭은 잘 어울리지 못한다.

  • 배열은 제네릭 타입, 매개변수화 타입, 타입 매개변수로 사용할 수 없다.
  • new List<E>[], new List>String>[], new E[] 와 같이 작성하면 컴파일 할 때 제네릭 배열 생성 오류를 일으킨다.
  • 제네릭 배열을 만들지 못하게 막은 이유는 타입이 안전하지 못하기 때문이다.
  • 이를 허용한다면 컴파일러가 자동 생성한 형변환 코드에서 런타임에 오류가 발생할 수 있다.

예시

// 제네릭 배열 생성을 허용하지 않는 이유 - 컴파일되지 않는다.
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)

(1)이 허용될 경우의 일어나는 일

  1. (2)는 원소가 하나인 List<Integer>을 생성
  2. (3)은 (1)에서 생성한 List<String>의 배열을 Object 배열에 할당
    • 배열은 공변이니 가능하다
  3. (4)는 (2)에서 생성한 List<Integer>의 인스턴스를 Ojbect 배열의 첫 원소로 저장한다.
    • 제네릭은 소거 방식으로 구현되어서 이 역시 성공한다
    • 즉 런타임에는 List<Integer> 인스턴스의 타입은 단순히 List가 되고 List<Integer>[] 인스턴스의 타입은 List[]가 된다.
  4. 문제가 발생하는 지점
    • 컴파일러는 꺼낸 원소를 자동으로 String을 형변환 하는데, 원소는 Integer이므로 ClassCastException이 발생한다.

위에서 발생하는 일을 막으려면 (1)에서 컴파일 오류를 내야 한다.

Chooser 클래스 예시

컬렉션 안의 원소 중 하나를 무작위로 선택해 반환하는 메서드

  • 주사위판, 매직 9볼, 몬테카를로 시뮬레이션용 데이터 소스 등으로 사용할 수 있다.

제네릭을 사용하지 않은 버전

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 메서드를 호출할 때마다 반환된 Obect를 원하는 타입으로 형 변환해야 함
    • 만약 타입이 다른 원소가 들어 있었다면 형변환 오류를 일으킬 수 있음

제네릭으로 만들기 위한 시도

// 컴파일 되지 않는다
public class Chooser<T> {
	private final T[] choiceArray;
	
	public Chooser(Collection<T> choices) {
		choiceArray = choices.toArray();
	}

	// choose는 그대로다.
}

컴파일하면 choiceArray = choices.toArray(); 부분에서
Object[] cannot be converted to T[] 에러가 난다.

choiceArray = (T[]) choices.toArray();와 같이 Object 배열을 T 배열로 형변환 하면 해결 된다.

하지만 T가 무슨 타입인지 알 수 없으니 이 형변환이 런타임에도 안전한지 보장할 수 없다는 경고가 뜰 것이다.

리스트를 사용한 방법 (권장)

  • 비검사 형변환 경고를 제거하려면 배열 대신 리스트를 쓰면 된다.

    publc class Chooser<T> {
    	private final List<T> choiceList;
    	
    	public Chooser(Collection<T> choices) {
    		coiceList = new ArrayList<>(choices);
    	}
    
    	public T choose() {
    		Random rnd = ThreadLocalRandom.current();
    		return choiceList.get(rnd.nextInt(choiceList.size()));
    	}
    
    }
  • 코드양이 조금 늘었고 조금 더 느림

  • 하지만 런타임에 ClassCastException을 만날 일이 없으니 그만한 가치가 있다.


💡 핵심 정리

  • 배열과 제네릭에는 매우 다른 타입 규칙이 적용된다
    • 배열은 공변이고 실체화되는 반면, 제네릭은 불공변이고 타입 정보가 소거된다.
    • 그 결과 배열은 런타임에는 타입이 안전하지만 컴파일 타임에는 그렇지 않다. (제네릭은 그 반대다)
  • 그래서 둘을 섞어 쓰기는 쉽지 않다.
  • 둘을 섞어 쓰다가 컴파일 오류나 경고를 만나면, 가장 먼저 배열을 리스트로 대체하는 방법을 적용해보자.

0개의 댓글