배열보단 리스트를 사용하라

수박참외메론·2022년 12월 28일
0

배열과 제네릭 타입에는 중요한 두가지 차이가 있다.
1. 배열은 공변(covariant)이지만, 제너릭은 불공변이라는 점
2. 배열은 실체화되지만, 제너릭은 실체화가 불가능하다.

공변

공변이란?

SubSuper의 하위 타입이라면 배열 Sub[]Super[] 타입의 하위 타입이 되는 이런 성질을 공변이라고 한다.

  • 공변 : StringObject의 서브타입이면 String[]Object[]의 서브타입이다.
  • 불공변 : StringObject의 서브타입이지만, List<String>List<Object>의 서브타입이 아니다.

그래서 아래와 같이 공변성이 만족이 되면

Object[] objectArray = new Long[1];
objectArray[0] = "asdf"; //ArrayStoreException을 던짐

이런 코드는 문법상 허용되지만, Long 형 배열에 string을 넣은 실수가 runtime에서야 오류가 발견된다.

아래와 같이 공변성이 충족이 되지않으면 애초에 컴파일이 되지 않는다.

List<Object> ol = new ArrayList<Long>(); //호환되지 않은 타입
ol.add("asdf");

실체화

배열은 런타임에 실체화

Object[] objectArray = new Long[1];

배열은 런타임에 타입이 실체화되기 때문에 Object[] 배열은 런타임에 Long[] 이 된다.

제네릭 타입은 런타임에 소거

List<String> ol = new ArrayList<String>(); 

그에비해 제너릭 타입은 런타임에는 그냥 타입이 소거된 ArrayList 만 타입으로 남게 된다. 원소타입은 위에서 봤듯이 컴파일 타임에서만 검사한다.

이러한 소거는 제네릭이 지원되기 전에 레거시 코드와 제네릭 타입을 함께 사용할 수 있게 해주는 메커니즘으로, 자바5가 제네릭으로 순조롭게 전환될 수 있도록 해줬다.

제너릭 배열을 막은 이유

타입 안전하지 않기 때문이다.
이를 허용한다면 컴파일러가 자동 생성한 형변환 코드에서 런타임에 ClassCastException이 발생할 수 있다.

List<String>[] stringLists = new List<String>[]; //(1)
List<Integer> intList = List.of(42);			//(2)
Object[] objects = stringLists;					//(3)
objects[0] = intList;							//(4)
String s = stringLists[0].get(0);				//(5)
  1. 제네릭 배열을 생성하는 (1) 이 허용된다고 가정해보자.
  2. (2) 는 원소가 하나인 리스트를 생성한다.
  3. (3) 은 (1) 에서 생성한 리스트배열을 Object 배열에 할당한다.
  4. (4)는 (2) 에서 생성한 배열의 첫 원소로 저장 -> 여기까지 문제없이 성공
  5. 하지만 get을 하는 순간, String형의 리스트에 저장되어있던 42 정수를 꺼내올때, 컴파일러는 자동으로 원소를 String 형으로 형변환하는데, 이는 runtime에 ClassCastException을 발생시킨다.

그래서 고로 제네릭 배열이 생성되지 않도록 (1)에서 컴파일 오류를 내야한다.

배열을 제너릭으로 만들 수 없어 귀찮을 때도 있다.
1. 제네릭 컬랙션 -> 해당 원소타입의 배열로 반환은 보통 불가능
2. 제네릭 타입과 가변인수 메서드를 함께 쓰면 해석하기 어려운 경고메세지를 받음

그래서 리스트를 사용하면 좋은점?

public class Chooser {
	private final Object[] choiceArray;
	
    public Chooser(Collection choices) {
    	choiceArray = choices.toArray();
    }
    
    public Object choose() {
    	Random rnd = ThreadLocalRandom.current();
        return choiceArray[rnd.nextInt(choiceArray.length)];
    }
}

위의 클래스를 사용하려면 choose메서드를 호출할 때마다 반환된 Object를 원하는 타입으로 형변환해줘야 한다. 혹시나 타입이 다른 원소가 들어있었다면 런타임에 형변환 오류가 날 것이다.
이 클래스를 제네릭으로 만들어보자.

public class Chooser<T> {
	private final T[] choiceArray;
	
    public Chooser(Collection<T> choices) {
    	choiceArray = choices.toArray();
    }
}

작성하였지만, 컴파일 되지 않는다. choices.toArray() 여기에서 Object[]T[]로 변환될 수 없다(incompatible type) 라고한다.

그래서 Object 배열을 T형 배열로 간단하게 형변환 시켜준다.

choiceArray = (T[]) choices.toArray();

그랬더니 이번엔 경고가 뜬다.

Chooser.java:9: warning: [unchecked] unchecked cast

결국에 비검사 형변환 경고를 제거하려면 배열대신 리스트를 쓰면 된다.

public class Chooser<T> {
	private final List<T> choiceArray;
	
    public Chooser(Collection<T> choices) {
    	choiceArray = new ArrayList<>(choices);
    }
    
    public Object choose() {
    	Random rnd = ThreadLocalRandom.current();
        return choiceArray[rnd.nextInt(choiceArray.length)];
    }
}

결론

배열과 제네릭은 공변성과 실체화에 있어서 매우 큰 차이를 보인다. 그 결과 배열은 런타임에 타입 안전하지 않지만, 제네릭(리스트)는 컴파일 타임에 검사가 다 되므로 리스트를 되도록 사용하자.

profile
하루하루는 성실하게 인생전체는 되는대로

0개의 댓글