-
28. 배열보다는 리스트를 사용하라JAVA/Effective java 2021. 3. 13. 14:48
- 배열과 제네릭 타입에는 중요한 차이가 두 가지 있다.
공변
- 첫 번째, 배열은 공변이다. Sub가 Super의 하위 타입이라면 배열 Sub[]는 Super[]의 하위 타입이 된다.
- (공변 : 함께 변한다)
- 반면 제네릭은 불공변이다. 서로 다른 타입 Type1과 Type2가 있을 때, List은 List의 하위 타입도 아니고 상위 타입도 아니다.
- 이때 문제가 있는 건 배열쪽이다.
- 어느 쪽이든 Long 용 저장소에 String을 넣을 수는 없다. 배열에서는 그 실수를 런타임에야 알게 되지만,
- 리스트를 사용하면 컴파일 때 바로 알 수 있다.
실체화
- 배열은 런타임에도 자신이 담기로 한 원소의 타입을 인지하고 확인한다.
- 앞 서 코드에서 보듯 Long 배열에 String을 넣으려 하면 ArrayStoreException이 발생한다.
- 반면 제네릭은 타입 정보가 런타임에는 소거( erasure ) 된다. 원소 타입을 컴파일 타임에만 검사하며 런타임에는 알수조차 없다.
- 이러한 소거는 제네릭이 지원하기 전의 레거시 코드와 제네릭 타입을 함께 사용할 수 있게 해주는 메커니즘으로 자바 5가 제네릭으로 순조롭게 전환될 수 있도록 해주었다.
- 이러한 주요 차이로 인해 배열과 제네릭은 잘 어우러지지 못한다.
- 예컨대 배열은 제네릭 타입, 매개변수화 타입, 타입 매개변수로 사용할 수 없다.
new List<E>[], new List<String>[], new E[]
다음의 코드들은 작성 시 컴파일에 제네릭 배열 생성 오류를 일으킨다.
제네릭 배열을 만들지 못하게 막은 이유는 무엇일까?
- 타입 안전하지 않기 때문이다.
- 이를 허용하면 컴파일러가 자동 생성한 형변환 코드에서 런타임에 ClassCastException이 발생할 수 있다.
- 런타임에 ClassCastException이 발생하는 일을 막아주는 제네릭 타입 시스템에 취지에 어긋나게 된다.
- 다음의 코드로 구체적인 상황을 살펴보자.
- 제네릭 배열을 생성하는 (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>를 사용하면 해결된다. 코드가 조금 복잡해지고 성능이 나빠질 수도 있지만 타입 안정성과 상호 운용성은 좋아진다.
- 생성자에서 컬렉션을 받는 Chooser 클래스를 예로 살펴보자.
- 이 클래스를 사용하려면 choose 메서드를 호출할 때마다 반환된 Object를 원하는 타입으로 형변환해야 한다.
- 혹시 타입이 다른 원소가 들어 있었다면 런타임에 형변환 오류가 날것이다.
- 이 클래스를 제네릭으로 만들어보자.
- T가 무슨 타입인지 알 수 없으니 컴파일러는 이 형변환이 런타임에도 안전한지 보장할 수 없다는 메세지다.
- 제네릭에서는 앞 서 말했듯이 원소의 타입 정보가 소거되어 런타임에는 무슨 타입인지 알 수 없다.
- 비검사 형변환 경고를 제거하기 위해 배열 대신 리스트를 써보자.
- 다음 Chooser는 오류나 경고 없이 컴파일 된다.
- 코드양이 조금 늘고 조금 더 느릴 테지만, 런타임에 ClassCastException을 만날 일은 없으니 그만한 가치가 있다.
핵심 정리
- 배열과 제네릭에는 매우 다른 타입 규칙이 적용된다.
- 배열은 공변이고 실체화되는 반면, 제네릭은 불공변이고 타입 정보가 소거된다.
- 그 결과 배열은 런타임에는 타입 안전하지만 컴파일타임에는 그렇지 않다. 제네릭은 그와는 반대다.
- 둘을 섞어 쓰기란 쉽지 않으며 섞어 쓰다가 컴파일 오류나 경고를 만나게 된다면, 가장 먼저 배열을 리스트로 대체하는 방법을 생각하자.
참고 자료
'JAVA > Effective java' 카테고리의 다른 글
30. 이왕이면 제네릭타입으로 만들라2 (0) 2021.03.15 29. 이왕이면 제네릭 타입으로 만들라 (0) 2021.03.14 27. 비검사 경고를 제거하라. (0) 2021.03.12 26. 로(Raw) 타입은 사용하지 말라 (0) 2021.03.11 25. 톱레벨 클래스는 한 파일에 하나만 담으라 (0) 2021.03.09