배열보다는 리스트를 사용하라
배열과 제네릭 차이 1
배열
- 배열은
공변
이다. (함께 변한다는 뜻이다.) - Sub가 Super의 하위 타입이라면 배열 Sub[]는 배열 Super[]의 하위 타입이 된다.
제네릭
- 불공변이다.
- 즉, 서로 다른 타입 Type1과 Type2가 있을 때,
List<Type1>
은List<Type2>
의 하위 타입도 아니고 상위 타입도 아니다.
이것만 보면, 제네릭에 문제가 있다고 생각할 수도 있지만, 사실 문제가 있는 건 배열 쪽이다.
아래 코드를 보자.
배열 예시코드 : 런타임에 실패한다.
위 코드는 컴파일 단계에서는 문제가 없지만 런타임 단계에서 문제가 발생한다.
제네릭 예시코드 : 컴파일되지 않는다.
List를 사용하면, 컴파일 단계에서 문제를 알 수 있게 된다.
배열과 제네릭 차이 2
배열
- 배열은 실체화(reify)된다.
- 배열은 런타임에도 자신이 담기로 한 원소의 타입을 인지하고 확인한다.
그래서 위의 '배열 예시코드'에서 보듯 Long 배열에 String을 넣으려 하면 arrayStoreException이 발생한다.제네릭
- 타입 정보가 런타임에는 소거(erasure)된다.
- 원소 타입을 컴파일타임에만 검사하며 런타임에는 알수조차 없다는 뜻이다.
- 소거는 제네릭이 지원되기 전의 레거시 코드와 제네릭 타입을 함께 사용할 수 있게 해주는 메커니즘이다.
제네릭 배열은 사용불가하다.
이상의 주요 차이로 인해 배열과 제네릭은 잘 어우러지지 못한다.
배열은 제네릭 타입, 매개변수화 타입, 타입 매개변수로 사용할 수 없다.
즉 코드를 new List
[], new List [], new e[] 식으로 작성하면 컴파일할 때 제네릭 배열 생성 오류를 일으킨다.
제네릭 배열을 만들지 못하게 막은 이유
타입 안전하기 않기 때문이다.
제네릭 배열을 허용하면, 컴파일러가 자동 생성한 형변환 코드에서 런타임에 ClassCastException이 발생할 수 있다.
런타임에 ClassCastException이 발생하는 일을 막아주겠다는 제네릭 타입 시스템의 취지에 어긋나는 것이다.
제네릭 배열 생성을 허용하지 않는 이유 - 컴파일되지 않는다.
제네릭 배열을 생성하는 코드 (1)가 허용이 된다고 가정해보자.
- (2)는 원소가 하나인 List
를 생성한다. - (3)는 Object 배열에 할당한다. 배열은 공변이니 문제가 없다.
- (4)는 (2)에서 생성한 List
의 인스턴스를 Object 배열의 첫 원소로 저장한다. - 제네릭은 소거 방식으로 구현되어서 이 역시 성공한다.
- 즉, 런타임에는
List<Integer>
타입은 List가 되고
List<Integer>[]
는 List[]가 된다.
- (5)에서 문제가 발생한다.
(4)에서 String만 담기로 했는데 Integer가 들어가 있으니 ClassCastException이 발생한다.
- 그리고 (5)는 이 배열의 처음 리스트에서 첫 원소를 꺼내려한다.
- 컴파일러는 꺼낸 원소를 자동으로 String으로 형변환하는데, 이원소는 Integer이므로 런타임에 ClassCastException이 발생한다.
위와 같은 이유로 이런 일을 방지하기 위해서 컴파일 오류를 내야 한다.
실체화 불가 타입(non-reifiable type)
E, List<E>
, List<String>
같은 타입을 실체화 불가 타입이라고 한다.
실체화 되지 않아서 런타임에는 컴파일타임보다 타입 정보를 적게 가지는 타입이다.
실체화될 수 있는 타입
소거 메커니즘으로 인해 매개변수화 타입 가운데 실체화될 수 있는 타입도 있다. List<?>
, Map<?, ?>
같은 비한정적 와일드카드만 가능하다.
배열의 불편함
배열을 제네릭으로 만들 수 없어 귀찮을 때도 있다.
- 제네릭 컬렉션에서는 자신의 원소 타입을 담은 배열을 반환하는게 불가능하다.
- 제네릭 타입과 가변인스 메서드(varargs method)를 함께 쓰면 해석하기 어려운 경고 메시지를 받게 된다.
- 가변인수 메서드를 호출할 때마다 가변인수를 담는 배열이 만들어지는데,
이때 그 배열의 원소가 실체화 불가 타입이면 경고가 뜬다. 이 문제는 @SafeVarargs로 해결한다.
배열 대신 리스트를 사용하자.
배열 단점
코드가 조금 복잡해지고 성능이 살짝 나빠질 수도 있다.
배열 장점
그 대신 타입 안정성과 상호운용성은 좋아진다.
제네릭을 사용하지 않은 코드 : Chooser 클래스
생성자에서 컬렉션을 받는 Chooser 클래스를 예로 살펴보자.
이 클래스는 컬렉션 안의 원소 중 하나를 무작위로 선택해 반환하는 choose 메서드를 제공한다.
생성자에 어떤 컬렉션을 넘기느냐에 따라 이 클래스를 주사위판, 매직 8볼, 몬테카를로 시뮬레이션용 데이터 소스 등으로 사용할 수 있다.
제네릭을 쓰지 않고 구현한 가장 간단한 버전이다
이 클래스를 사용하려면 choose 메서드를 호출할 때마다 반환된 Object를 원하는 타입으로 형변환해야 한다.
- 혹시나 타입이 다른 원소가 들어 있었다면 런타임에 형변환 오류가 날 것이다.
아래와 같이, 잘못 변환하면 런타임 형변환 오류가 발생한다.
Chooser를 제네릭으로 만들기 위한 첫 시도 - 컴파일되지 않는다.
- 제네릭을 추가하고, choose() 메서드는 그대로 유지했다.
- 타입 컴파일 오류가 발생한다.
Chooser를 제네릭으로 만들기 위한 두번째 시도 - 경고가 뜬다.
- Object 배열을 T 배열로 형변환했다.
- T가 무슨 타입인지 알 수 없으니 컴파일러는 이 형변환이 런타임에도 안전한지 보자알 수 없다는 경고 메시지를 띄운다.
제네릭에서는 원소의 타입 정보가 소거되어 런타임에는 무슨 타입인지 알 수 없다.
실행했을 때 동작한다. 단지 컴파일러가 안전을 보장하지 못할 뿐이다.
리스트 기반으로 작성 - 타입 안정성 확보
위에서 발생한 비검사 형변환 경고를 제거하려면 배열 대신 리스트를 사용하면 된다.
- 리스트로 바꾸게 되면 오류나 경고 없이 컴파일된다.
- 이번 버전은 코드 양도 증가하고 살짝 느릴 수는 있다.
- 하지만,
런타임에 ClassCastException을 만날 일은 없다.
☑️ 핵심정리
내용정리를 하기 전에 핵심 포인트를 짚어보자.
- 배열과 제네릭은 매우 다른 타입 규칙이 적용된다.
- 배열은 공변이고 실체화되기에 런타임에 타입 안전하지만 컴파일에는 그렇지 않다.
- 제네릭은 불공변이고 타입 정보가 소거되기에 컴파일에 안전한다.
- 배열과 제네릭을 섞어서 쓰다가 컴파일 오류나 경고를 만나면 배열을 리스트로 대체하자.
'Dev Books > Effective Java' 카테고리의 다른 글
[item 25] 톱레벨 클래스는 한 파일에 하나만 담으라 (0) | 2024.06.17 |
---|---|
[item 27] 비 검사 경고를 제거하라 (0) | 2024.05.31 |
[item 26] 로 타입은 사용하지 말라 (0) | 2024.05.31 |
[item 24] 멤버 클래스는 되도록 static으로 만들라. (0) | 2024.04.05 |
[item 3] private 생성자나 열거 타입으로 싱글턴임을 보증하라 (1) | 2024.01.14 |
댓글