반응형

ITEM 28 "배열보다는 리스트를 사용하라"

 

배열과 제네릭 타입에는 중요한 차이가 두 가지가 존재한다.

첫 번째, 배열은 공변이고 제네릭은 불공변이다. 공변의 뜻은 "같이 변한다"는 뜻인데 원래 타입의 상하 관계가 컬렉션에 삽입이 되어도 살아 있다는 뜻이다. 따라서 배열 Sub[] 는 배열 Super[] 의 하위 타입이 되고, List<Sub>와 List<Super> 는 독립적인 타입으로 인식한다.

 

// 런타임 오류
Object[] test = new Long[1];
test[0] = "test";
        
// 컴파일 불가
List<Object> o1 = new ArrayList<Long>();
o1.add("test");

 

 

런타임 시에 오류를 확인하는 것보다 컴파일 시에 오류를 확인하는 것이 더 좋다.

두 번째는 배열은 실체화가 된다. 배열은 런타임에도 자신의 원소 타입을 인지하고 확인할 수 있다. 그래서 위 Long 형 test 배열에 String 을 넣으면 런타임 오류(ArrayStoreException) 가 발생하는 것이 이 때문이다. 반면, 제네릭은 타입 정보가 런타임에는 소거가 된다. 컴파일 타임에만 검사하며 런타임에는 알 수가 없다.

 

두 가지 차이로 인해 배열과 제네릭은 어울리지 못 한다. 그래서 배열은 제네릭 타입, 매개변수화 타입, 타입 매개변수로 사용할 수 없다. 예를 들어 new List<E>[], new List<String>, new E[] 가 컴파일이 되지 않는다. 타입 안전하지 않기 때문에 이를 허용한다면 런타임 시에 ClassCastException 이 발생할 수 있다. 제네릭이 런타임에 ClassCastException 을 방지하기 위해 불공변을 지원하는데 그 취지에 어긋나는 것이다. 배열과 제네릭을 함께 사용했을 때 벌어지는 일을 살펴보자.

 

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

 

1번째 문장에서 제네릭 배열을 생성한다고 가정했다. 3번째 문장에서는 배열은 공변이기 때문에 문제 없이 할당된다. 4 번째 문장은 제네릭이 소거 방식으로 구현되기 때문에 List<integer> 가 List 로 되어 충분히 할당된다. 문제는 5번째 문장이다. List<String> 인스턴스만 담겠다고 선언한 stringLists 배열에 List<Integer> 인스턴스가 저장되어 있다. get 으로 첫 번째 원소를 꺼낼 때 자동으로 String 으로 형변환되는데 Integer 를 형변환 하였으니 ClassCastException 이 발생한다. 섞어 쓰지 말고 배열을 리스트로 대체하는 방안을 생각해보자.

 

제네릭에서는 원소의 타입 정보가 소거되어 런타임에는 무슨 타입인지 알 수 없다는 점과 배열은 공변이고 실체화된다는 사실을 꼭 기억하자. 둘의 성격이 반대라서 컴파일 오류나 경고를 만난다면 배열을 리스트로 바꾸자.

반응형

+ Recent posts