Study/Effective-Java

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

hongeeii 2023. 11. 30.
728x90
반응형

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

배열과 제네릭 타입의 차이

  1. 배열은 공변이다.
  2. 제네릭은 불공변이다.
//        Object[] objArr = new Long[1];
//        objArr[0] = "타입이 다르다."; // ArrayStoreException 발생

        List<Object> ol = new ArrayList<Long>();
        ol.add("타입이 다르다.");
image

배열에서는 런타임할 때 알게되지만, 리스트를 사용하게 되면 컴파일할 때 바로 알 수 있다.

주요차이점은 배열은 실체화(reify)된다. -> 배열과 제네릭은 잘 어우러지지 못한다.

배열은 런타임에도 자신이 담기로 한 원소의 타입을 인지하고 확인하다.

그래서 위의 코드에서 보듯 Long배열에 String을 넣으려 하면 ArrayStoreException이 발생한다.

반면, 제네릭은 타입 정보다 런타임에는 소거된다.
원소 타입을 컴파일타임에만 검사하며 런타임에는 알 수 조차 없다는 뜻.

        List<String> names = new ArrayList<>();
        names.add("dongjin");
        String name = names.get(0);
        System.out.println(name);
        
//////// 컴파일러가 만들어주는 코드 (예시)
        List names = new ArrayList();
        names.add("dongjin");
        Object o = names.get(0);
        String name = (String) o;
        System.out.println(name);

소거는 제네릭이 지원되기 전의 레거시 코드와 제네릭 타입을 함께 사용할 수 있게 해주는 메커니즘.

image
//         공변
        Object[] anything = new String[10];
        anything[0] = 1;

        // 불공변
        List<String> names = new ArrayList<>();
//                List<Object> objects = names;

        List<String>[] stringLists = new ArrayList<String>[1]; // 만약 가능하다면
        List<Integer> intList = List.of(42);
        Object[] objects = stringLists;
        objects[0] = intList;
        String s = stringLists[0].get(0);
//        System.out.println(s);
image

원래는 위 이미지처럼 제네릭 배열이 생성되지 않도록 첫 번째 줄에서 컴파일 오류를 내야 한다.

실체화 불가 타입

E, List<E>, List<String> 같은 타입을 실체화 불가 타입(non-reifiable type)이라 한다.

실체화가 되지 않아서 런타임에는 컴파일타임보다 타입 정보를 적게 가지는 타입이다.

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

배열을 비한정적 와일드카드로 만들 수는 있지만, 유용하게 쓰일 일은 없다.

배열을 썼을 때 문제점과 리스트를 썼을 때 장점

Chooser 클래스를 예시로 만들어서 설명.

public class Chooser {
    private final Object[] choiceList;

    public  Chooser(Collection choices){
        choiceList = choices.toArray();
    }

    public Object choose(){
        Random rnd = ThreadLocalRandom.current();
        return choiceList[rnd.nextInt(choiceList.length)];
    }

    public static void main(String[] args) {
        List<Integer> list = List.of(1, 2, 3, 4, 5, 6);
        Chooser chooser = new Chooser(list);

        for (int i = 0; i < 10; i++) {
            Number choice = (Number) chooser.choose();
            System.out.println(choice);
        }
    }
}

잘 작동함.

        List<String> list = List.of("dong", "kim");
        Chooser chooser = new Chooser(list);

        for (int i = 0; i < 10; i++) {
            Number choice = (Number) chooser.choose();
            System.out.println(choice);
        }

이렇게 바뀐다면 컴파일에선 오류가 나지 않지만 런타임 시에 ClassCastException이 발생한다.


    private final T[] choiceList;

    @SuppressWarnings("uncheked")
    public Chooser(Collection<T> choices) {
        choiceList = (T[]) choices.toArray();
    }

    public Object choose() {
        Random rnd = ThreadLocalRandom.current();
        return choiceList[rnd.nextInt(choiceList.length)];
    }

이렇게 @SuppressWarning("unchecked")를 붙이는 방법도 있지만

배열을 리스트로 바꾸는 방법이 더 좋다.

public class Chooser<T> {
    private final List<T> choiceList;

    @SuppressWarnings("uncheked")
    public Chooser(Collection<T> choices) {
        choiceList = new ArrayList<>(choices);
    }

    public Object choose() {
        Random rnd = ThreadLocalRandom.current();
        return choiceList.get(rnd.nextInt(choiceList.size()));
    }

    public static void main(String[] args) {
        List<Integer> list = List.of(1, 2, 3, 4, 5, 6);
        Chooser<Integer> chooser = new Chooser<>(list);

        for (int i = 0; i < 10; i++) {
            Number choice = (Number) chooser.choose();
            System.out.println(choice);
        }
    }
}

이렇게 하면 List에 다른 타입이 들어가면 컴파일단계에서 오류가 나기 때문에 안정성이 좋아진다.

만약 배열과 리스트를 섞어 쓰다가 오류나 경고를 만나면, 가장 먼저 배열을 리스트로 대체하는 방법을 적용해보자.

@SafeVarags

  • 생성자와 메소드의 제네렉 가변인자에 사용할 수 있는 어노테이션

제네릭 타입과 가변인수 메소드를 함께 쓰면 경고 메시지를 받게 된다.

가변인수 메서드를 호출할 때마다 가변인수 매개변수를 담을 배열이 하나 만들어지는데, 이때 그 배열의 원소가 실체화 불가 타입이라면 경고가 발생하는 것이다.

이 문제는 @SafeVarags로 대처할 수 있다.

package org.example;

import java.util.ArrayList;
import java.util.List;

public class Dangerous {

    @SafeVarargs // Not safe
    static void dangerous(List<String>... stringLists) {
        List<Integer> integerList = List.of(42); // List<String>... => List[] 리스트 배열이니까 오브젝트 배열에 할당할 수 있다. 공변이니까.
        Object[] objects = stringLists;
        objects[0] = integerList;   // 힙 오염 발생
        String s = stringLists[0].get(0);   // ClassCastException
    }

    @SafeVarargs
    static <T> void safe(T... values) {
        for (T value : values) {
            System.out.println(value); // 새로운 변수에 할당하지 않고, 리턴하지 않고 단순 출력할 때만 @SafeVarargs 쓰는 것을 권장.
        }
    }

    public static void main(String[] args) {
        List<String> stringList = new ArrayList<>();
        stringList.add("a");
        stringList.add("b");
        stringList.add("c");

        List<String> stringListB = new ArrayList<>();
        stringListB.add("a");
        stringListB.add("b");
        stringListB.add("c");

        List<Integer> integerList = new ArrayList<>();
        integerList.add(1);
        integerList.add(2);
        integerList.add(3);

        safe(stringListB, stringList, integerList);
        dangerous(stringList, stringListB);
    }
}
image
728x90
반응형

추천 글