배열과 제네릭 타입에는 중요한 두 가지 차이가 있다
공변이란 함께 변한다는 뜻이고, Sub가 Super의 하위타입일 때,
Sub[]이 Super[]의 하위 타입이 되는 것이다
Crew를 상속하는 BackendCrew와 FrontendCrew가 있다고 하자
public class Crew {}
public class BackendCrew extends Crew{}
public class FrontendCrew extends Crew{}
Crew[] crews = new BackendCrew[10];
crews[0] = new FrontendCrew(); // 여기서 에러를 던진다
이 코드는 컴파일 타임에는 Crew[]
, 런타임에는 BackendCrew[]
가 된다
따라서 런타임에 ArrayStoreException
을 발생시킨다
제네릭으로 생성하면 이런 실수를 컴파일 때 발견할 수 있다
List<Crew> crews = new ArrayList<BackendCrew>(); // 호환 불가로 컴파일되지 않는다
crews.add(new FrontendCrew());
BackendCrew용 저장소에 FrontendCrew를 넣을 수 없는 건 둘 다 동일하다
하지만 제네릭으로 생성했을 때가 컴파일 때 바로 알 수 있다
배열은 런타임에 실체화되지만, 제네릭은 타입 정보가 런타임에 소거된다
코드 상으로 보면 아래와 같다
Crew[] crews = new BackendCrew[10];
crew는 런타임에 BackendCrew[]가 된다
반면 제네릭 타입은 원소 타입을 컴파일 시에만 검사한다
// 컴파일 타임(실제 작성한 코드)에서는 원소 타입을 검사한다
ArrayList<BackendCrew> backendCrews = new ArrayList<BackendCrew>();
ArrayList<FrontendCrew> frontendCrews = new ArrayList<FrontendCrew>();
// 런타임에는 제네릭 타입이 소거되어서 구분이 불가능하다
ArrayList backendCrews = new ArrayList();
ArrayList frontendCrews = new ArrayList();
이런 차이로 배열과 제네릭을 함께 쓰기 힘들다
배열은 제네릭 타입, 매개변수화 타입, 타입 매개변수로 사용할 수 없는데, 타입 안전하지 않기 떄문이다
앞서 코드로 보였듯, 실체화되지 않아 런타임에 컴파일 타임보다 타입 정보를 적게 가지는 타입이다
E, List<E>, List<String>
같은 타입이 해당된다
이들을 배열로 만들 수 없어 아래와 같은 불편한 점이 존재한다
- 제네릭 컬렉션에서 자신의 원소 타입을 담은 배열을 반환하는 게 보통은 불가능하다
아이템 33에서 해결법을 찾을 수 있다 - 가변인수 메서드는 호출할 때 마다 매개변수를 담은 배열을 만드는데, 제네릭을 사용할 경우 경고가 발생한다
아이템 32에서 더 자세한 설명을 볼 수 있다
배열로 형변환 할 때 제네릭 배열 생성 오류나 비검사 형변환 경고가 뜰 때가 있다
대부분은 배열인 E[] 대신 컬렉션인 List를 사용하면 해결된다
코드가 조금 복잡해지고 성능이 살짝 나빠질 수 있지만, 타입 안전성과 상호운용성은 좋아진다