Item28. 배열보다는 리스트를 사용하라!
배열과 제네릭 타입의 차이
1. 공변 / 불공변
- 배열은 공변(
covariant
)이다.Sub
가Super
의 하위 타입이라면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번에서
컴파일 오류(제네릭 배열 생성 불가)
를 낸다.
- 1,2,3,4를 수행하고 나면,
실체화 불가 타입(non-reifiable type)
E
,List<E>
,List<String>
같은 타입을 실체화 불가 타입이라 한다.- 실체화 되지 않아서 런타임에는 컴파일 타임보다 타입 정보를 저게 갖는다.
- 소커 매커니즘이 적용되기 때문에, 매개변수화 타입 가운데 실체화 가능 타입은
List<?>
,Map<?,?>
같은 비한정적 와일드카드 타입뿐이다.
배열을 제네릭으로
- 배열을 제네릭으로 만들 수 없어 귀찮을 때도 있다.
제네릭 컬렉션에서는 자신의 원소 타입을 담은 배열 반환이 보통 불가능하다.
- 제네릭 타입과 가변인수 메서드를 함께 사용 시, 해석이 어려운 경고 메세지를 받는다.
- 가변인수 메서드 호출마다 가변인수 매개변수를 담을 배열이 하나 만들어지는데, 그 배열의 원소가 실체화 불가 타입일 경우 경고 발생
- 이러한 문제는
@SafeVarargs
애너테이션으로 대처할 수 있다.
- 이러한 문제는
- 가변인수 메서드 호출마다 가변인수 매개변수를 담을 배열이 하나 만들어지는데, 그 배열의 원소가 실체화 불가 타입일 경우 경고 발생
배열로 형변환
- 배열로 형변환 시, 제네릭 배열 생성 오류나 비검사 형변환 경고가 뜨는 경우
- 대부분
E[]
배열 대신 컬렉션인List<E>
를 사용하면 해결된다.- 코드의 복잡도가 올라가고, 성능 하락이 있을 수 있다.
- 타입 안전성과 상호운용성은 좋아진다.
- 대부분
핵심 정리
- 배열과 제네릭에는 매우 다른 타입 규칙이 적용된다.
- 배열은 공변이고 실체화된다.
- 제네릭은 불공변이고 타입 정보가 소거된다.
- 배열은 런타임에 타입 안전하지만 컴파일타임에는 그렇지 않다.
- 제네릭은 배열과 반대다.
- 배열은 공변이고 실체화된다.
- 이러한 이유로 배열, 제네릭 타입을 섞어 쓰기란 쉽지 않다.
- 둘을 섞어 쓰다가 컴파일 오류나 경고를 만나는 경우
- 가장 먼저 배열을 리스트로 대체하자.
느낀 점
다익스트라 문제를 풀면서 List 배열을 사용할 일이 있었는데, 이 경우 꺽쇠의 사용을 피하면서 임시방편으로 생성해 사용했다.
그러면서 ‘왜 제네릭 타입 배열을 생성할 수 없는 것인지’ 항상 궁금했다.
이번 아이템을 통해 왜 안되는 것인지 알 수 있게 되었다.
공부하느라 지치지만 점점 지식을 채워지고, 시야가 확장되고 있다는 것을 느끼게 되어 학습에 보람을 느꼈다😀👍
Comments powered by Disqus.