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

new_wisdom·2021년 2월 17일
2

📕 Java 📕

목록 보기
18/24
post-thumbnail

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

정리

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

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

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

참고 자료

profile
블로그 이사중 🚛

0개의 댓글