Language/Java

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

JOYERIM 2023. 5. 9. 01:36

 

 

 

 

 

 

배열 제네릭
공변(Sub가 Super의 하위 타입이라면 배열 Sub[]는 배열 Super[]의 하위 타입이 된다.) 불공변(서로 다른 타입 Type1과 Type2가 있을 때, List은 List의 하위 타입도 아니고 상위 타입도 아니다.)
런타임 단계에서 발견 컴파일 단계에서 발견
실체화(런타임에도 자신이 담기로 한 원소의 타입을 인지하고 확인) 실체화 x(원소 타입을 컴파일 타임에만 검사하여 런타임에는 알 수 없음)
제네릭 타입, 매개변수화 타입, 타입 매개변수 사용 불가(cuz Type Safety)  

 

 

 

실체화 타입

  • 컴파일 타임과 런타임에 동일한 타입 정보를 가지는 타입
  • 반대로 실체화 불가 타입은 런타임에는 타입 정보가 소거되어 컴파일 타임보다 타입 정보를 적게 가지는 타입

실체화, 소거

String[] stringArray = new String[] { "실체화 타입" };
List<String> stringLIst = Arrays.asList( "실체화 불가 타입" );

공변

Object[] objectArray = new Long[1];
objectArray[0] = "타입이 달라 넣을 수 없다.";

불공변

List<Object> ol = new ArrayList<Long>(); // Object만 넣을 수 있음, <>로 수정하면 됨
ol.add("타입이 달라 넣을 수 없다.");

 

 

 

소거(type erasure)

 

 

제네릭이 처음 도입됐을 때, 하위 호환성 문제를 해결하기 위해 도입된 메커니즘이다. 제네릭이 도입되기 전에 작성된 코드와 제네릭을 함께 사용할 수 있게 해주는 역할을 한다.

제네릭이 지원되기 전의 코드는 모두 Raw Type으로 처리된다. 따라서 컴파일러는 제네릭을 사용하는 코드에서 타입 파라미터의 정보를 소거하고, 로 타입으로 대체한다. 즉, 컴파일 시점에는 제네릭 타입 정보가 모두 사라지고, 로 타입으로 변환된다.

소거를 통해 제네릭 타입이 로 타입으로 변환되기 때문에, 제네릭 타입을 사용하는 코드는 제네릭 타입의 실제 타입 인자를 알 수 없게 된다. 따라서 제네릭 타입의 실제 타입 인자에 대한 정보를 런타임 시점에서는 사용할 수 없게 된다.

 

 

 

 

 

 

 

 

 

제네릭 배열 생성이 안되는 이유

 

 

 

public static void main(String[] args) {
  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)이 허용된다고 가정하자. (3)은 (1)에서 생성한 List<String>을 원소로 담고 있는 배열을 Object[]에 할당한다. List<String> 또한 Object이므로 할당할 수 있다. 즉, 런타임에는 List<Integer> 인스턴스 타입은 List, List<Integer>[] 인스턴스 타입은 List[]가 된다. 따라서 ArrayStoreException이 발생하지 않는다. (5)에서 'ClassCastException'이 발생한다.

결국 List<String> 타입의 인스턴스만 담겠다는 의도가 있지만, 실제로는 List<Integer> 타입의 인스턴스도 담을 수 있게 되므로, 제네릭 타입 안정성이 깨지게 된다. 따라서 제네릭 배열 생성을 허용하지 않는다.

 

 

 

 

 

 

배열보다 리스트를 권장하는 이유

 

 

 

아래 코드는 choiceArray 배열의 랜덤한 원소를 반환한다.

 

 

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)];// 형변환 오류 발생 가능
  }
}

 

 

이 코드에서는 Collection의 제네릭 타입이 명시되어 있지 않고, 로 타입을 사용한다. 그러나 toArray() 메소드에서는 제네릭 타입 정보를 보존하지 않는다 따라서 choiceArray는 Object 배열로 만들어지며, choose() 메소드에서 해당 배열의 원소를 반환할 때 형변환 오류가 발생할 수 있다.

 

 

 

public class Chooser<T> {
  private final T[] choiceArray;

  public Chooser(Collection<T> choices) {
    choiceArray = choices.toArray();// Object[] cannot be converted to T[]
  }

  public Object choose() {
    Random rnd = ThreadLocalRandom.current();
    return choiceArray[rnd.nextInt(choiceArray.length)];
  }
}

 

 

 

위 코드는 형 변환 오류를 막기 위해 제네릭 클래스로 변경한 코드이다. 그러나 컴파일 에러가 뜨며, Object[]를 T[]로 형 변환하여도 Unchecked cast 경고가 뜬다. 물론, 개발자가 안전하다고 확신할 수 있다면 주석과 에너테이션을 달아 경고를 숨길 수 있으나, 원인을 제거하는 것이 좋다.

 

 

 

 

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

 

 

 

위는 배열 대신 리스트를 사용하여 원인을 제거한 코드이다. 즉, 배열로 형변환할 때 제네릭 배열 생성 오류나 비검사 형변환 경고가 뜨는 경우 대부분은 배열인 E[] 대신 컬렉션인 List<E>를 사용하면 해결된다.