Item 28. 배열보다는 리스트를 사용하라

심규환·2022년 3월 1일
0

Effective Java

목록 보기
26/29

배열과 제네릭 타입의 차이는 무엇일까? 크게 두 가지가 있는데. 바로 공변실체화이다. 먼저 공변이란 함께 변한다는 뜻인데. 만약 Super 타입이 있고 Sub 타입이 Super 타입의 하위 타입이라고 해보자. 그러면 Super[] 에는 Super 타입뿐아니라 하위타입인 Sub 타입도 넣을 수 있게 된다.
반면 제네릭 타입의 경우, 처음 지정한 타입만 넣을 수 있게 해준다. 하위 타입을 넣고 싶다면 extends를 사용하면 된다. 하지만 일반적으로 지정된 타입만 넣을 수 있는 불공변을 가지고 있다.

다음으로 실체화(reify)를 살펴보자. 실체화란 런타임 시에도 자신이 담기론 한 원소의 타입을 인지하고 확인할 수 있어야 한다. 배열의 경우, 런타임 중에도 처음 넣기로한 타입이나 하위 타입을 넣을 수 있게 터치를 해준다.
반면 제네릭의 경우, 원소 타입을 컴파일타임에만 검사하고 해당 정보를 런타임에는 소거 시킨다. 소거시키는 이유는 레거시 코드와의 호환을 위해서이다. 그렇기 때문에 컴파일 이후에는 일반적인 배열과 다름없게 만들어진다.

그러면 왜 배열보다는 리스트를 사용해야 할까??
바로 타입 안전성을 위해서이다. 그 이유를 자세히 살펴보자. 자바에서는 제네릭 배열을 만들지 못하게 막았다. 만약 이를 허용한다면 컴파일러가 자동 생성한 형변환 코드에서 런타임에 ClassCastException이 발생할 수 있다.

다음은 컴파일 되지 않는 코드이다.

List<String> stringLists = new List<String>[1];   // 제네릭 배열 생성
List<Integer> intList = List.of(42);     // Integer 타입 List 생성
Object[] objects = stringLists;      // Object[] 배열에 List<String>을 담는다.
objects[0] = intList;   // !! object[]는 List<String>으로 선언했는데. List<Integer>를 처음 원소로 넣었다.
String s = stringLists[0].get(0); // !! Integer 타입인 첫 번째 원소를 String 타입으로 넣었다.

만약 제네릭 배열을 위의 코드 제일 상단처럼 허용한다면 주석으로 넣는 것처럼, 마지막 코드를 실행할 때 컴파일러는 꺼낸 원소를 자동으로 String 타입으로 형변환 할 것이고 이는 결국 ClassCastException 를 발생시킨다.

그러면 타입도 안전하고 싶은데 그냥 배열을 넣으면 안전하지 않고 그러면 제네릭타입으로 배열을 선언하고 싶은데 그건 허용하지 않으니 어떡해야 할까? Item 28의 제목처럼, 코드가 조금 복잡해지고 성능이 살짝 나빠질 수 있지만 List를 사용하자. 그러면 타입 안전성과 상호운용성을 보장 받을 수 있게 된다.

아래에 간단한 코드를 살펴보자. 컬렉션을 받고 컬렉션 안의 원소를 무작위로 선택해서 반환하는 코드이다.

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)];
    }
}

위의 코드를 보면 Object[] 형태의 필드가 존재하는데. choose() 메서드를 사용하면 반환할 때 마다 원하는 타입으로 형변환 해야 한다. 배열이기 때문에 다른 원소가 들어갈 수 있다. 그러면 형변환 시 오류가 날 수 있게 된다. 그러면 코드를 제네릭으로 만들어보자.

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

이렇게 코드를 구성하고 컴파일 해보면 경고가 발생한다. (T[]) choices.toArray() 부분에서 발생하는데.
T가 무슨 타입인지 모르기 때문에 형변환이 런타임에도 보장하지 않는다는 뜻이다. 런타임 중에는 T 타입은 소거 되기 때문이다. 이를 애너테이션을 사용하여 경고를 숨겨도 되지만 간단하게 리스트를 사용하면 해결된다.

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 choiceList.get(rnd.nextInt(choiceList.size()));
    }
}

코드가 다소 늘었고 조금 느려졌지만 런타임에 ClassCastException이 발생하지 않기 때문에 가치가 있다.

profile
장생농씬가?

0개의 댓글