Java - '배열보다는 리스트를 사용하라'

<이펙티브 자바>를 읽으며 정리

Posted by Yan on November 1, 2021

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

배열과 제네릭 타입의 차이점

1. 변할 수 있는가?

  • 배열은 함께 변한다. sub가 super의 하위타입이라면, sub[]는 super[]의 하위 타입이 된다.
  • 제네릭은 함께 변하지 않는다. 서로 다른 타입 Type1과 Type2가 있을 때, List<Type1>과 List<Type2>는 하위타입도 아니고 상위타입도 아니다.

배열을 쓰면 문제가 생길 수 있다

  • 배열에서 에러가 나면 런타임에서 알 수 있어, Exception을 던진다.
  • 리스트를 사용하면 컴파일 할때 에러를 바로 알 수 있다.

2. 실체화 되는가?

  • 배열은 실체화된다. 배열은 런타임에도 자신이 담기로 한 원소의 타입을 인지하고 확인한다.
  • 제네릭은 런타임에 타입 정보가 소거된다. 원소 타입을 컴파일타임에만 검사하며 런타임에는 알 수 없다.
  • 소거: 제네릭이 지원되기 전의 레거시 코드와 제네릭 타입을 함께 사용할 수 있게 해주는 메커니즘.

제네릭과 배열이 잘 어우러지지 못하는 이유

  • 배열은 제네릭타입, 매개변수화 타입, 타입 매개변수로 사용할 수 없다.
  • 제네릭 배열을 만들지 못하게 막은 이유: 타입 안전하지 않기 때문
    • 만약 제네릭 배열을 만들 수 있다면, 컴파일러가 자동 생성한 형변환 코드에서 런타임에 ClassCastException 발생
    • 런타임에 ClassCastException이 발생하는 일을 막겠다는 제네릭 타입 시스템의 취지에서 어긋나게 된다.

실체화 불가 타입 non-refiable type

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

배열은 제네릭으로 만들 수 없다.

  • 제네릭 컬렉션에서는 자신의 원소 타입을 담은 배열을 반환하는게 보통은 불가능하다.
  • 제네릭 타입과 가변인수 메소드를 함께 쓰면 해석하기 어려운 경고 메세지를 받게 된다.
  • 가변인수 메소드를 호출할 때마다 가변인수 매개변수를 담을 배열이 하나 만들어지는데, 이때 그 배열의 원소가 실체화 불가 타입이라면 경고가 발생하는 것이다.
  • 배열로 형변환할 때 배열 생성오류 또는 비검사 형변환 경고가 뜰 시, 대부분은 배열 E[] 대신 컬렉션 List<E>를 사용하면 해결된다.

제네릭이 필요한 코드 예시

1
2
3
4
5
6
7
8
9
10
11
12
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 메소드를 호출할 때마다 반환된 Object를 원하는 타입으로 형변환해야한다.

제네릭을 사용하여 수정한 코드

1
2
3
4
5
6
7
8
9
10
11
12
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()));
  }
}