자바

[Effective java] item 28. 배열보다는 리스트를 사용하라

newwisdom 2021. 8. 2. 02:35
반응형

2021-02-17 글 이사

우테코 Level 1 로또 미션에서 "배열 대신 ArrayList를 사용한다."가 요구사항에 들어있었다.
왜 배열대신 ArrayList를 사용하라는 것일까?
찾아보니 이펙티브 자바에서도 그 내용이 나와 있어서 스스로의 물음에 답하는 내용을 정리하려 한다.

Array(배열) vs ArrayList(리스트)

들어가기에 앞서 이펙티브 자바를 보기 전 Array과 ArrayList의 차이점을 간단히 적어본다.

Array(배열)

  • 사이즈가 정적인 데이터 구조이다. 일단 생성되면 크기를 변경할 수 없다.
  • 원시 타입과 객체 모두 원소로 포함할 수 있다.
  • for 또는 for-each 루프를 통해서 반복된다.
  • 길이에 대해 length 변수를 사용한다.
  • Generic(제네릭)을 사용 할 수 없다.
  • 원소를 할당하기 위해 할당 연산자=를 사용한다.

ArrayList(리스트)

  • 사이즈가 동적인 데이터 구조이다. 용량을 초과하는 요소를 추가하면 크기가 자동으로 증가한다.
  • 객체 원소만 포함할 수 있다.
  • 요소를 반복하는 iterators를 제공한다.
  • 길이에 대해 size() 메서드를 사용한다.
  • Generic(제네릭)을 지원한다.
  • 원소를 할당하기 위해 add() 메서드를 사용한다.
  • Collections가 제공하는 다양한 메소드들을 사용할 수 있다.

Effective Java item 28

배열과 제네릭 타입의 차이

배열은 공변이다.

SubSuper의 하위 타입이라면 배열 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> 같은 타입을 실체화 불가 타입이라 한다.
제네릭은 타입 소거로 인해 실체화되지 않아서 런타임에는 컴파일타임보다 타입 정보를 적게 가지는 타입을 뜻한다.
(매개변수화 타입 가운데 실체화 될 수 있는 타입은 비한정적 와일드카드 타입 뿐이다.)

정리

배열과 제네릭에는 매우 다른 타입 규칙이 적용되어서 둘을 섞어 쓰기란 쉽지 않다.
배열은 공변, 제네릭은 불공변이다.
이 말은 배열은 런타임 타입에 안전하지만 컴파일 타임에 안전하지 못하다.
제네릭은 그 반대이다.

단순히 사이즈가 정적인지 동적인지의 차이 뿐만 아니라 배열과 리스트에는 이렇게 많은
차이점이 존재하고 있었다.

만약 둘을 섞어 쓰다가 컴파일 오류나 경고를 만나면, 먼저 배열을 리스트로 대처하자!

참고 자료

  • [What is the difference between an array and an array list?
반응형