Home [Effective Java] Item28. 배열보다는 리스트를 사용하라!
Post
Cancel

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

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

배열과 제네릭 타입의 차이

1. 공변 / 불공변

  • 배열은 공변(covariant)이다.
    • SubSuper의 하위 타입이라면 Sub[]Super[]의 하위 타입이 된다.
  • 제네릭 타입은 불공변(invariant)이다.
    • Type1, Type2가 있을 때 List<Type1>List<Type2>의 하위 타입도, 상위 타입도 아니다.

Example

1
2
Object[] objects = new Long[1];
objects[0] = "응~ 타입이 달라서 못들어가~~~~"; // ArrayStoreException
1
2
List<Object> objects = new ArrayList<Long>(); // 호환 안됨.
objects.add("타입 달라달라~");
  • 배열의 경우, 런타임에 실패한다.
  • 제네릭의 경우, 컴파일이 되지 않는다.
    • 컴파일 시 잘못되었다는 것을 알아챌 수 있다.

2. 실체화(reify), 소거

  • 배열은 런타임 시점에 자신이 담기로 한 원소의 타입을 인지하고 실행(실체화)한다.
  • 제네릭은 런타임 시점에 타입 정보를 소거한다.
    • 애초에 제네릭은 컴파일 시점에 ClassCastException 같이 타입 관련 에러를 던질 위험을 제거했다.
      • 그래서 제네릭은 런타임시 타입을 굳이 가질 필요가 없고, 타입을 소거해도 된다.
    • 참고로, 소거는 제네릭이 지원되기 전의 레거시를 제네릭과 함께 사용할 수 있도록 돕기 위한 매커니즘이다.
      • 소거를 통해 자바 5가 제네릭으로 순조롭게 전환되었다.

배열과 제네릭 차이의 결과

  • 배열과 제네릭의 두 가지 주요 차이로 인배 배열과 제네릭은 잘 어울리지 못한다.
  • 배열은 제네릭 타입, 매개변수화 타입, 타입 매개변수로 사용할 수 없다.
    • new List<E>[], new List<String>[], new E[] 식으로 작성 시, 컴파일할 때 제네릭 배열 생성 오류을 던진다.

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

  • type-safe가 보장되지 않기 때문이다.
  • 배열 생성을 허용한다면, 컴파일러가 자동 생성한 형변환 코드에서 런타임에 ClassCastException이 발생할 수 있다.
    • 런타임에 ClassCastException이 발생한다? 그건 제네릭 타입 취지에 어긋남.

제네릭 배열이 가능하다 가정해보자

1
2
3
4
5
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
  • 2번에서 원소가 하나인 List<Integer>를 생성할 것이다.
  • 3번은 1번에서 생성한 List<String>의 배열을 Object 배열에 할당하는데, 배열은 공변이니 아무 문제가 일어나지 않는다.
  • 4번은 2번에서 생성한 List<Integer>의 인스턴스를 Object 배열의 첫 원소로 저장하는데, 제네릭은 소거 방식으로 구현되었기에 이 역시 성공한다.
    • 런타임 시, List<Integer>List가 된다. List<Integer>[]List[]이 된다.
  • 5번의 경우가 문제다.
    • 1,2,3,4를 수행하고 나면, List<String>만 담겠다고 선언한 List<String>[]stringLists 배열의 첫 원소에 List<Integer>가 들어가 있게 된다.
    • stringLists[0].get(0)를 통해 첫 원소를 꺼내게 되는데, 이때 컴파일러는 자동 형변환을 지원한다.
      • 본래 List<String>String으로 형변환
      • 하지만 현재 List<Integer>가 들어있으니, 형변환 시 ClassCastException이 발생!!
    • 이런 일을 방지하기 위해 1번에서 컴파일 오류(제네릭 배열 생성 불가)를 낸다.

실체화 불가 타입(non-reifiable type)

  • E, List<E>, List<String> 같은 타입을 실체화 불가 타입이라 한다.
  • 실체화 되지 않아서 런타임에는 컴파일 타임보다 타입 정보를 저게 갖는다.
  • 소커 매커니즘이 적용되기 때문에, 매개변수화 타입 가운데 실체화 가능 타입은 List<?>, Map<?,?> 같은 비한정적 와일드카드 타입뿐이다.

배열을 제네릭으로

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

  • 제네릭 타입과 가변인수 메서드를 함께 사용 시, 해석이 어려운 경고 메세지를 받는다.
    • 가변인수 메서드 호출마다 가변인수 매개변수를 담을 배열이 하나 만들어지는데, 그 배열의 원소가 실체화 불가 타입일 경우 경고 발생
      • 이러한 문제는 @SafeVarargs 애너테이션으로 대처할 수 있다.

배열로 형변환

  • 배열로 형변환 시, 제네릭 배열 생성 오류나 비검사 형변환 경고가 뜨는 경우
    • 대부분 E[] 배열 대신 컬렉션인 List<E>를 사용하면 해결된다.
      • 코드의 복잡도가 올라가고, 성능 하락이 있을 수 있다.
      • 타입 안전성과 상호운용성은 좋아진다.

핵심 정리

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

느낀 점

다익스트라 문제를 풀면서 List 배열을 사용할 일이 있었는데, 이 경우 꺽쇠의 사용을 피하면서 임시방편으로 생성해 사용했다.

그러면서 ‘왜 제네릭 타입 배열을 생성할 수 없는 것인지’ 항상 궁금했다.

이번 아이템을 통해 왜 안되는 것인지 알 수 있게 되었다.

공부하느라 지치지만 점점 지식을 채워지고, 시야가 확장되고 있다는 것을 느끼게 되어 학습에 보람을 느꼈다😀👍

This post is licensed under younghwani by the author.

[Effective Java] Item27. 비검사 경고를 제거하라!

[Effective Java] Item29. 이왕이면 제네릭 타입으로 만들라!

Comments powered by Disqus.