우테코 Level 1 로또 미션에서 "배열 대신 ArrayList를 사용한다."가 요구사항에 들어있었다.
왜 배열대신 ArrayList를 사용하라는 것일까?
찾아보니 이펙티브 자바에서도 그 내용이 나와 있어서 스스로의 물음에 답하는 내용을 정리하려 한다.
들어가기에 앞서 이펙티브 자바를 보기 전 Array과 ArrayList의 차이점을 간단히 적어본다.
for
또는 for-each
루프를 통해서 반복된다.length
변수를 사용한다.=
를 사용한다. size()
메서드를 사용한다.add()
메서드를 사용한다.Sub
가 Super
의 하위 타입이라면 배열 Sub[]
는 Super[]
의 하위 타입이 된다.
즉 함께 변한다는 말이다.
하지만 제네릭은 불공변으로 서로 다른 타입 Type1
, Type2
가 있을 때,
List<Type1>
은 List<Type2>
의 하위 타입도, 상위 타입도 아니다.
아래 코드는 문법상 허용은 되지만 런타임에 실패한다.
Object[] objectArray = new Long[1];
/* ArrayStoreException 발생 */
objectArray[0] = "타입이 달라 넣을 수 없음";
또 아래 코드는 컴파일 오류를 일으킨다.
List<Object> objectList = new ArrayList<Long>();
objectList.add("타입이 달라 넣을 수 없음");
두 코드 모두에서 Long용 저장소에 String을 넣을 수 없다.
배열은 이를 런타임에 알게 되지만 리스트는 컴파일 때 바로 알 수 있다.
배열은 런타임에도 자신이 담기로 한 원소의 타입을 인지하고 확인한다.
때문에 Long용 저장소에 String을 넣으려 하면 ArrayStoreException을 발생시킨다.
하지만 제네릭은 타입 정보가 런타임에는 소거된다.
이는 원소의 타입을 컴파일 타임에만 검사하며 런타임에는 알 수 없다는 것이다.
여기서 타입 정보의 소거라 함은 제네릭이 지원되기 전
레거시 코드와 제네릭 타입을 함께 사용할 수 있게 해준다.
그 이유는 타입 안전하지 않기 떄문이다.
제네릭 배열을 허용한다면 컴파일러가 자동 생성한 형변환 코드에서 런타임에 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)이 허용된다면 (2)는 원소가 하나인 List<Integer>
를 생성한다.
(3)은 (1)에서 생성한 List<String>
의 배열을 Object
배열에 할당한다.
배열은 공변이니 아무 문제가 없다.
(4) 번은 (2)에서 생성한 List<Integer>
의 인스턴스를 Object
배열의 첫 원소로 저장한다.
제네릭은 런타임 시점에서 타입 정보를 소거하니 List<Integer>
은 List
가 되고
List<Integer>[]
는 List[]
가 된다.
따라서 (4)에서도 ArrayStoreException이 발생하지 않는다.
(5)에서는 List<String>
인스턴스만 담겠다고 선언한 stringLists
배열에는
List<Integer>
인스턴스가 저장돼 있다.
(5)는 이 배열의 처음 리스트에서 첫 원소를 꺼내려 하는데 컴파일러는 꺼낸 원소를 자동으로 String으로
형변환 하는데, 이 원소는 Integer
이니 런타임에 ClassCastExceptiondl qkftodgksek.
이를 막기 위해서 제네릭 배열 생성을 막도록 (1)에서 컴파일 오류를 내야 한다.
E
, List<E>
, List<String>
같은 타입을 실체화 불가 타입이라 한다.
제네릭은 타입 소거로 인해 실체화되지 않아서 런타임에는 컴파일타임보다 타입 정보를 적게 가지는 타입을 뜻한다.
(매개변수화 타입 가운데 실체화 될 수 있는 타입은 비한정적 와일드카드 타입 뿐이다.)
배열과 제네릭에는 매우 다른 타입 규칙이 적용되어서 둘을 섞어 쓰기란 쉽지 않다.
배열은 공변, 제네릭은 불공변이다.
이 말은 배열은 런타임 타입에 안전하지만 컴파일 타임에 안전하지 못하다.
제네릭은 그 반대이다.
단순히 사이즈가 정적인지 동적인지의 차이 뿐만 아니라 배열과 리스트에는 이렇게 많은
차이점이 존재하고 있었다.
만약 둘을 섞어 쓰다가 컴파일 오류나 경고를 만나면, 먼저 배열을 리스트로 대처하자!