Programming/Java

[Effective Java] item28 - 배열보다는 리스트를 사용하라

VSFe 2021. 3. 24. 18:24
배열보다는 리스트를 사용하라!

앞 아이템에서 살짝 설명했는데, 배열은 공변성을 갖고 있고 리스트는 그렇지 않다. 다시 말해서, sub와 super가 상/하위 클래스 관계라면, sub[]는 super[]의 하위 타입이 되는 것이다.

Object[] arr = new Long[140];
arr[0] = "들어갈까?"; // 들어갈리가 없다.

'문법'상 틀린 코드는 아니지만, 상식적으로 들어갈리가 없다는걸 알 것이다. 이런 경우 런타임시 ArrayStoreException이 발생하는데, 이는 런타임 과정에서 자신이 담기로 한 원소의 타입을 체킹해서 예외를 반환하는 것이다.

앞서 제네릭은 공변성을 갖고 있지 않다고 했는데, 따라서

List<Object> list = new ArrayList<Long>();
list.add("들어갈까?");

의 경우 컴파일 타임에서 잡아준다. 참고로 제네릭은 런타임 과정에선 타입 정보를 날려버리기 때문에, (기존 레거시 코드와의 호환을 위해) 원소 타입을 컴파일 타임에만 체킹한다.

이런 차이 때문에 배열과 제네릭을 동시에 사용하기엔 좀 곤란하다. 실제로 new List<E>[]new E[] 같은 코드는 오류를 뱉는데, 왜 그럴까? 타입 안전하지 않기 때문이다. 만약 허용한다면 컴파일러가 자동 생성한 형변환 코드에서 ClassCastException을 반환할 수 있는데, 우리는 이걸 막기 위해 제네릭을 쓴게 아니었나?

List<String>[] stringLists = new List<String>[1];
List<Integer> intList = List.of(42);
Object[] objects = stringLists;
objects[0] = intList;
String s = stringLists[0].get(0);

만약 첫 줄이 허용된다면, (즉 제네릭 배열 생성이 가능하다면) 뭔가 이상하는 생각이 들 네번째 줄도 무사히 넘어갈 수 있다. (비록 제네릭이 공변하지 않다고 해도, List의 경우 런타임 과정에선 타입 정보가 제거되므로 stringLists나 intList나 동일하게 List 타입이기 때문이다. 그러나 마지막 줄에선 String으로 형변환을 시도하나 Integer이기 때문에 ClassCastException이 터져버린다! 실제 상황에선 별로 발생하지 않을 것 같아도 실제 발생 가능성은 배제할 수 없다.

일반적으로 E, List<E>, List<String> 같은 친구들을 실체화 불가 타입 (Non-Reifiable Type) 이라고 부르는데, 이는 실체화되지 않아서 런타임에 타입 정보가 적은 타입들이다. 그나마 실체화 될 수 있는 타입은 비한정적 와일드카드 타입 뿐인데, 이걸로 배열을 만들기엔 유용하게 쓰이기 어렵다.

그러다보니 귀찮은 상황을 자주 겪는다. 제네릭 컬렉션에선 원소 타입을 담은 배열을 뱉기도 어렵고, (파이썬에서의 dict.items() 같은 것들!!) 가변적인 변수를 받게 되면 경고 메세지도 뿜는다. (일반적으로 가변 인자는 자신들을 배열의 형태로 뱉는데, 이 과정에서 배열의 원소가 실체화 불가 타입이면 경고를 뿜는다.)

결국 배열로 형변환 하는 과정에서 제네릭 배열 오류나 비검사 형변환 경고가 뜬다면 E[]을 List<E>로 바꿔서 해결할 수 있다. (물론 코드가 조금 길어지고 약간의 성능 저하가 발생할 수 있다.)