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

2025. 1. 19. 13:58

배열 vs 리스트

 

첫 번째 차이,

배열은 공변이다. -> Sub가 Super의 하위 타입이면, Sub[]는 Super[]의 하위타입이다.

제네릭은 불공변이다. -> List<Sub>와 List<Super>는 서로 다른 타입이다.

 

문제는 배열에 있다.

public static void main(String[] args) {
    Object[] objects = new Long[1];
    objects[0] = "haha";
}
Exception in thread "main" java.lang.ArrayStoreException: java.lang.String

Long 배열에 String을 넣으려고 하니 런타임 에러가 터진다.

 

ArrayStoreException : Thrown to indicate that an attempt has been made to store the wrong type of object into an array of objects. 

: 배열에 잘못된 타입의 객체를 넣으려 시도했을 때 터지는 에러

 

List<Object> list = new ArrayList<Long>();
Required type:List<Object>
Provided:ArrayList<Long>

제네릭을 사용하면 컴파일 에러가 나온다.

 

 

두 번째 차이,

배열은 실체화 된다.

배열은 런타임에도 자신이 담기로 한 원소의 타입을 인지하고 확인한다. 즉, 소거를 진행하지 않는다.

반면, 제네릭은 타입 정보가 런타임에는 소거된다. 타입을 컴파일 시에만 체크하고 런타임 시에는 체크하지 않는다. 

 

 

위의 차이들로 인해 배열과 제네릭은 잘 어우러지지 못한다.

배열은 제네릭 타입, 매개변수화 타입, 타입 매개변수로 사용할 수 없다. 즉, 제네릭과 배열을 같이 사용할 수 없다.

new List<E>[], new List<String>[], new E[] 모두 불가능.

그렇다면 제네릭 배열을 왜 못만들게 막았을까? 타입 안전하지 않기 때문이다. 만약 제네릭 배열이 가능하다면 런타임에 ClassCastException이 발생할 수 있다. 이는 제네릭 타입 시스템의 취지에 어긋난다.

//아래 코드는 컴파일 시 경고만 나오고 에러는 나오지 않는다.
//런타임 시에 타입 소거로 인해 List list = new ArrayList();가 되기 때문.
List<String> list = new ArrayList();

//로 타입으로 생성 시 에러는 안나옴. but, 런타임 에러 터짐.
List[] stringLists = new List[1];
List<Integer> intList = List.of(42);
Object[] objects = stringLists;
objects[0] = intList;
String s = stringLists[0].get(0);

List<String>[] stringLists = new List<>[1];
//컴파일 에러 Array creation with '<>' not allowed

 

 

E, List<E>, List<String> 같은 타입을 실체화 불가 타입이라 한다.

실체화 불가 타입: 런타임에 타입 정보가 소거되는 타입.

실체화 가능한 타입: 배열(int[]), 원시 타입(int, long), 비한정적 와일드카드 타입(?)

실체화 불가능 타입: 구체적 매개변수화 타입(List<String>), 한정적 와일드카드 타입(<? extends Number>)

 

소거 매커니즘 때문에 매개변수화 타입 가운데 실체화 될 수 있는 타입은 List<?>, Map<?,?> 같은 비한정적 와일드카드 타입 뿐이다.

 

 

//제네릭을 사용 안하면 cast 시 에러 발생할 수 있다.
public class RandomNumberPicker {
    private final Object[] numbers;

    public RandomNumberPicker(Object[] numbers) {
        this.numbers = numbers;
    }

    public Object pick() {
        int index = (int) (Math.random() * numbers.length);
        return numbers[index];
    }

    public static void main(String[] args) {
        RandomNumberPicker numberPicker = new RandomNumberPicker(new Integer[]{1, 2, 3, 4, 5});
        int pick = (int) numberPicker.pick();
        String str = (String) numberPicker.pick();
        System.out.println("numberPicker.pick() = " + numberPicker.pick());
    }
}

//제네릭을 사용 하면 컴파일 경고는 나오지만, 타입 안전하다.
public class RandomNumberPicker<E> {
    private final E[] numbers;

    public RandomNumberPicker(Collection<E> numbers) {
        this.numbers = (E[]) numbers.toArray();
    }

    public E pick() {
        int index = (int) (Math.random() * numbers.length);
        return numbers[0];
    }

    public static void main(String[] args) {
        RandomNumberPicker<Integer> numberPicker = new RandomNumberPicker<>(List.of(1, 2, 3));
        int pick = (int) numberPicker.pick();
        String str = (String) numberPicker.pick(); //컴파일 에러
        System.out.println("numberPicker.pick() = " + numberPicker.pick());
    }
}

//배열 대신 리스트를 사용해서 컴파일러 경고도 제거하였다.
public class RandomNumberPicker<E> {
    private final List<E> numbers;

    public RandomNumberPicker(Collection<E> numbers) {
        this.numbers = new ArrayList<>(numbers);
    }

    public E pick() {
        Random rnd = ThreadLocalRandom.current();
        return numbers.get(rnd.nextInt(numbers.size()));
    }

    public static void main(String[] args) {
        RandomNumberPicker<Integer> numberPicker = new RandomNumberPicker<>(List.of(1, 2, 3));
        int pick = numberPicker.pick();
        System.out.println("numberPicker.pick() = " + numberPicker.pick());
    }
}

 

정리

1. 배열은 공변이고 실체화된다. 리스트는 불공변이고 타입 정보가 소거된다.

2. 이에 따라 배열은 런타임에는 타입 안전하지만 컴파일 시에는 불안전하다. 리스트는 컴파일 시에는 타입 안전하지만 런타임 시 타입 불안전하다.

3. 그래서 배열과 리스트를 섞어 쓰기는 쉽지 않다.

4. 둘을 섞어 쓰다가 컴파일 오류나 경고를 만나면 가장 먼저 배열을 리스트로 대체하는 방법을 적용해봐라.

BELATED ARTICLES

more