이펙티브자바, 28. 배열보다는 리스트를 사용하라
배열과 리스트의 차이
- 배열의 경우 Sub[] 이 Super[] 의 하위타입이 된다. 그러니까 Object[] 가 String[] 의 하위타입이 된다.
- 이러한 배열의 특성 때문에 문법 상 맞지 않는 코드가 컴파일 과정에서 승인된다. 그리고 런타임 과정에서 예외가 발생한다.
- 리스트, 정확히 제너릭 클래스와 인터페이스는, 컴파일 시점에서 타입이 맞지 않는 코드에 대하여 에러를 던진다.
- 추가적으로 Object[] 타입으로 String[]이 초기화되는 것과 달리, 제네릭 클래스와 인터페이스는 상위/하위타입으로 타입변화가 허용되지 않는다.
- 타입의 표현력과 안정성문제로 배열보다는 리스트가 선호된다.
@Test
void test() {
Object[] objets = new Integer[10];
objets[0] = 1;
Assertions.assertThatThrownBy(()->{
// String 을 삽입 할 수 있다.
objets[1] = "hello!";
}).isInstanceOf(ArrayStoreException.class);
}
@Test
void test2() {
// 컴파일 에러 발생
// List<Object> objects = new ArrayList<Integer>();
// Integer로 타입이 고정된다.
List<Integer> objects = new ArrayList<Integer>();
objects.add(123);
// 컴파일 에러 발생
objects.add("kim");
}
제네릭으로의 적용의 다양한 방법들
제네릭 미적용
- 제네릭이 아닌 Object 배열을 사용한다.
- Object 배열을 사용할 경우 형변환의 문제가 발생한다.
public class Chooser1 {
private final Object[] choiceArray;
public Chooser1(Collection choices) {
choiceArray = choices.toArray();
}
public Object choose() {
Random rnd = ThreadLocalRandom.current();
return choiceArray[rnd.nextInt(choiceArray.length)];
}
}
제네렉 배열의 사용
- 제네릭을 배열로 한다.
T[]
- 제네릭은 런타임 시점에서 타입을 잃어버린다. 그러므로 타입변환을 코드에서 명시해야 한다.
(T[]) choices.toArray();
- 형변환을 하기 때문에 컴파일러는 형변환과 관련하여 확신을 할 수 없고 경고 메시지를 띄운다.
public class Chooser2<T> {
private final T[] choiceArray;
public Chooser2(Collection<T> choices) {
choiceArray = (T[]) choices.toArray();
}
public Object choose() {
Random rnd = ThreadLocalRandom.current();
return choiceArray[rnd.nextInt(choiceArray.length)];
}
}
리스트 제네릭 사용
- 형변환 문제과 관련한 고민이 사라진다. 컴파일 시점에서 코드의 문제가 드러난다. 성능 등 문제로 배열을 꼭 써야하는 상황이 아니면 리스트가 낫다.
public class Chooser3<T> {
private final List<T> choiceArray;
public Chooser3(Collection<T> choices) {
choiceArray = new ArrayList<>(choices);
}
public Object choose() {
Random rnd = ThreadLocalRandom.current();
return choiceArray.get(rnd.nextInt(choiceArray.size()));
}
}
제네릭의 특징
- 컴파일 시점에만 데이터 타입에 대한 정보를 제네릭은 가지고 있다. 런타임 시점에서 데이터 타입에 대한 정보가 사라지는 것을 소거(erasure)라 한다. 그리고 이러한 특징으로 제너릭을 실체화 불가 타입이라 한다.
- 이러한 방식으로 인하여 제너릭이 존재하지 않았던 레거시 코드와의 호환성을 가지게 되었다. 더 나아가 컴파일 시점에서 제너릭과 관련한 에러를 잡아낼 수 있게 되었다.
- 한편, 배열은 런타임에서도 데이터타입에 대한 정보를 가지고 있다. Integer[]는 런타임에서도 Integer 데이터 타입을 가지고 있다. 이러한 패러다임의 차이로 제너릭 배열이 허용되지 않는다.
교훈?
- 제네릭을 학습하며 캐스팅을 하지 않는 것이 얼마나 좋은지를 느끼게 한다.
Integer var = (Integer) result;
라는 식의 형변환 자체는, 형변환의 대상의 데이터 타입 자체가 불완전함을 보여준다. - 하지만 제네릭은 명확하게
List<Integer> var = result
; 라는 형태를 가지기 때문에 코드가 명확하다. 캐스팅이 코드 작성과 유지보수 입장에서 어떤 문제를 가지는지를 이해하는 계기가 되었다. 이것 하나만으로도 큰 장점이 있어 보인다.