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

gang_shik·2022년 5월 16일
0

Effective Java 5장

목록 보기
3/6
  • 배열과 제네릭 타입에는 중요한 차이가 두 가지 있음

    • 배열공변(Covariant)임, SubSuper 의 하위 타입이라면 배열 Sub[]Super[] 의 하위 타입이 되고 함께 변함

    • 제네릭불공변(invariant)임 서로 다른 타입 Type1Type2 가 있을 때 List<Type1>List<Type2> 의 하위 타입도 상위 타입도 아님

  • 이를 보면 문제가 제네릭에 있어 보이지만 실제로는 배열에 있음, 아래의 예시를 볼 수 있음

// 런타임에 실패함
Object[] objectArray = new Long[1];
objectArray[0] = "타입이 달라 넣을 수 없다."; // ArrayStoreException을 던짐
// 컴파일이 되지 않음
List<Object> ol = new ArrayList<Long>(); // 호환되지 않는 타입임
ol.add("타입이 달라 넣을 수 없다.");
  • 즉, 어느 쪽이든 Long 용 저장소에 String 을 넣을 수는 없음, 이때 배열에선 그 실수를 런타임에서야 알게 되지만, 리스트 사용시 컴파일 할 때 바로 알 수 있음

  • 두 번째 주요 차이로, 배열은 실체화 됨 즉, 배열은 런타임에도 자신이 담기로 한 원소의 타입을 인지하고 확인함

  • 그래서 위처럼 Long 배열에 String 을 넣으려 하면 ArrayStoreException 이 발생함, 반면 앞서 이야기했듯 제네릭타입 정보가 런타임에는 소거

  • 즉, 원소 타입을 컴파일타임에만 검사하며 런타임에는 알수조차 없다는 뜻임

  • 이와 같은 차이로 배열과 제네릭은 잘 어우러지지 못함, 배열제네릭 타입, 매개변수화 타입, 타입 매개변수로 사용할 수 없음

  • 이처럼 제네릭 배열을 만들지 못하게 막은 이유는 타입 안전하지 않기 때문임, 이를 허용한다면 컴파일러가 자동 생성한 형변환 코드에서 런타임에 ClassCastException 이 발생할 수 있음

  • 하지만 런타임에 ClassCastException 이 발생하는 일을 막아주겠다는 제네릭 타입 시스템의 취지에 어긋남

// 컴파일되지 않음
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) 이 허용된다고 할 때 (2) 는 원소가 하나인 List<Integer> 를 생성함

  • (3)(1) 에서 생성한 List<String> 의 배열을 Object 배열에 할당함, 배열은 공변이니 문제 없음

  • (4)(2) 에서 생성한 List<Integer> 의 인스턴스를 Object 배열의 첫 원소로 저장함

  • 제네릭은 소거 방식으로 구현되어서 이 역시 성공함

  • 즉, 런타임에는 List<Integer> 인스턴스의 타입은 단순히 List 가 되고, List<Integer>[] 인스턴스의 타입은 List[] 가 됨 따라서 (4) 에서도 ArrayStoreException 을 일으키지 않음

  • 이 다음부터가 문제인데 List<String> 인스턴스만 담겠다고 선언한 stringLists 배열에는 지금 List<Integer> 인스턴스가 저장돼 있음, 그리고 (5) 는 이 배열의 처음 리스트에서 첫 원소를 꺼내려함

  • 컴파일러는 꺼낸 원소를 자동으로 String 으로 형변환하는데, 이 원소는 Integer 이므로 런타임에 ClassCastException 이 발생함, 이런 일을 방지하려면 제네릭 배열이 생성되지 않도록 (1) 에서 컴파일 오류를 내야함

  • E , List<E> , List<String> 같은 타입을 실체화 불가 타입(non-reifiable type)이라함, 쉽게 말해 실체화되지 않아서 런타임에는 컴파일타임보다 타입 정보를 적게 가지는 타입

  • 소거 메커니즘 때문에 매개변수화 타입 가운데 실체화될 수 있는 타입List<?>Map<?,?> 같은 비한정적 와일드카드 타입뿐임(배열도 만들 수 있지만 유용하게 쓰일 일이 거의 없음)

  • 배열을 제네릭으로 만들 수 없어 귀찮을 때도 있음, 예를 들어 제네릭 컬렉션에서는 자신의 원소 타입을 담은 배열을 반환하는 게 보통은 불가능함

  • 또한 제네릭 타입과 가변인수 메서드(varargs method)를 함께 쓰면 해석하기 어려운 경고 메시지를 받게됨

  • 가변인수 메서드를 호출할 때마다 가변인수 매개변수를 담을 배열이 하나 만들어지는데, 이 때 그 배열의 원소가 실체화 불가 타입이라면 경고가 발생하는 것임(이 문제는 @SafeVarargs 애너테이션으로 대처가능)

  • 배열로 형변환할 때 제네릭 배열 생성 오류나 비검사 형변환 경고가 뜨는 경우 대부분은 배열인 E[] 대신 컬렉션인 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)];
		}
}
  • 위 클래스를 생성자에 어떤 컬렉션을 남기냐에 따라 다양한 방식으로 활용할 수 있음, 여기서 제네릭을 활용해서 이를 처리할 수 있음

  • 이 클래스 사용을 위해 choose 메소드를 호출할 때마다 반환된 Object 를 원하는 타입으로 형변환해야 함

  • 혹시나 타입이 다른 원소가 들어 있었다면 런타임에 형변환 오류가 날 것임

  • 이 때 제네릭에서는 원소의 타입 정보가 소거되어 런타임에는 무슨 타입인지 알 수 없음을 기억하면 좋음

  • 그리고 만약 컴파일러가 안전을 보장하지 못할 경우 코드를 작성하는 사람이 안전하다고 확신한다면 주석을 남기고 애너테이션을 달아 경고를 숨겨도 됨(하지만 애초에 경고의 원인을 제거하는 편이 나음)

  • 그래서 아래와 같이 제네릭 활용과 리스트를 통해서 위의 클래스에 제네릭을 활용할 수 있음

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()));
		}
}
profile
측정할 수 없으면 관리할 수 없고, 관리할 수 없으면 개선시킬 수도 없다

0개의 댓글