본문 바로가기
독서 감상문/Effective Java

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

by 프람6 2024. 5. 9.
작성자: 프람
작성 일시: 2024.05.09
내용: Effective Java 3/E 아이템-28

인사

안녕하세요.👋 이번주도 역시나 여러모로 너무 바쁜 나날들을 보내고 있네요.

그래도 할건 해야겠죠??

이번 주제는 '아이템28-배열보다는 리스트를 사용하라'입니다.

퐈이팅 넘치게 시작해보겠씁니다 💪


배열 VS 리스트

우선 배열과 리스트의 차이점으로 시작해보겠습니다.

 

  • 배열은 공변(covariant)이다/ 제네릭은 불공변(invariant)이다
  • 배열은 실체화(reify) 된다/ 제넥릭은 소거(erasure)된다
  • 배열은 제네릭 타입, 매개변수화 타입, 타입 매개변수로 사용할 수 없다

이렇게 글로 정리하니 너무 어려워 보이지만, 우린 개발자니까~~

코드로 보면 쉽게 이해할 수 있습니다. 

 

1.  배열은 공변(covariant)/ 제네릭은 불공변(invariant) 

 

코드를 보면 딱 감이 오시죠? 

Object[] ObjectArray = new Long[1];

 

배열은 할당 받은 타입에 의해, 런타임에 배열의 타입이 시시각각 바뀔 수 있다는 것입니다.

때문에, 그 아랫 줄에서 런타임에러로 'ArrayStoreException'을 던져줍니다.

objectArray[0] = "타입이 달라 넣을 수 없다";

 

즉, 공변은 런타입에 하위 타입으로 변경될 수 있다는 뜻입니다.
반대로 불공변은 컴파일 타입에 확실히 타입이 지정되고 더 이상 바뀔 수 없다는 뜻이겠죠?

 

 

그렇기 때문에 위의 코드는 아무런 문제없이 잘 돌아가죠!

 

2. 배열은 실사화 / 제네릭은 소거

 

이것도 딱히 어려운것은 아닌데 '아이템 26. 로 타입은 사용하지 말라'를 참고하면 이해하기 더 편해요.

 

로 타입은 사용하지 말라

작성자: 프람작성 일시: 2024_05_06내용: Effective java 3/E 5장 아이템26 이펙티브 자바 Effective Java 3/E - 예스24자바 플랫폼 모범 사례 완벽 가이드 - Java 7, 8, 9 대응자바 6 출시 직후 출간된 『이펙티브

fram-tech.tistory.com

짧게 여기서 다시 설명하자면, 

제네릭은 하위 호환성을 위해 컴파일러가 컴파일 타임에 제네릭 타입을 Object 타입으로 형 변환 시켜줍니다. 

이것을 소거라고 하는 것이죠.

 

반면, 배열은 런타임까지 본인의 타입을 스스로 알고 있다는 것이에요.

이러한 내부 원리를 알게되었으니 다시 1번의 내용 공변성과 불공변성이 이해가 더 쉽게 갈거에요. 

 

3. 배열은 제네릭 타입, 매개변수화 타입, 타입 매개변수로 사용할 수 없다

즉 , 아래 표와 같은 문법은 지원하지 않습니다.

이름 예시 코드
제네릭 타입 new List<E>[]
매개변수화 타입 new List<String>[]
타입 매개변수 new E[]

 

 

코드로도 살펴보죠.

 

만약, (1)이 허용된다고 가정해봅시다.

좀 복잡해보이겠지만, 

결국은 배열은 공변(covariant)/ 제네릭은 불공변(invariant)에서의 예제와 같이 ArrayStoreException을 발생시킵니다.

 

그럼  아까 '2번 배열은 실사화한다'라는 설명이 틀린거 아닌가요?

'(List<String>)타입의 배열인데 왜 배열을 컴파일 타입에 잡아줘?'라고  생각을 할 수도 있는데요.

 

제네릭이 주는 가장 큰 이점 중 하나가 무엇인지 아시나요?

바로 컴파일 타입에 컴파일러가 타입을 확인해주기 때문에 타입 안정성을 보장해주며 코드를 가독성있게 쓸 수있다는 것인데요.

위 표와 같은 문법(Syntax)들이 지원된다면, 제네릭이 주는 장점이 모두 사라지겠죠? 

 

그래서 자바에서는 E, List<E>, List<String>은 별도로 실체화 불가 타입(non-reifiable type)으로 분류합니다.

(즉, 런타임이 컴파일타임보다 더 적은 정보를 가지고 있음을 뜻합니다)


실사화 불가 타입의 불편함

곰곰이 생각해보니new E[] 또는 new Array<Object>[]이 지원된다면, 더 편하지 않을까? 라는 생각이 들지 않으신가요? 

또는, 제넥릭 타입과 가변인수 메서드(vargargs method)[1]를 함께 쓰면 해석하기 어려운 경고 메세지를 받을 수도 있습니다.

이 문제는 @SafeVarargs 애너테이션으로 해결이 됩니다. 이는 추후에 더 하세하게 다루겠습니다.

 

각 불편함에 대해 코드로 더 자세하게 알아 보겠습니다.

 

1.  배열을 제네릭으로 만들 수 없을 때의 귀찮음

우선 다면체 주사위 클래스를 배열로 간단하게 만들어 보겠습니다

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

 

제네릭을 구현하지 않은 위 Chooser 클래스는 형변환을 사용할 때마다 번거롭게 해줘야합니다.

또 잘못된 형변환을 시도할 시 런타임에서야 예외를 발생시킬 것이죠.

 

이러한 문제를 해결하기 위해 우리는 하나의 꾀를 내어 보기로 했습니다.

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

 

잘 작동할까요? 

앞서 말씀드린것과 같이 T[]는 실사화 불가 타입에 해당하기 컴파일오류를 뿜어주고 있네요.

실행시켜 더 자세한 오류 메세지를 살펴 봅시다.

 

 

T의 타입이 불분명하니 형변환이 런타임에도 안전한지 보장할 수 없다는 메세지입니다.😭

힝.. 그럼 어떻게 해야할까요?? 

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

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

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

 

배열은 탐색 속도가 O(1)이지만 List는 O(n)입니다. 하지만 타입 안정성을 위해서 이정도 속도는 충분히 트레이드 오프를 할만하다고 할 수 있죠!! 

 

2. 가변인수 메서드를 함께 쓰면 해석하기 어려운 경고 메세지를 받을 수 있음의 귀찮음

힙 오염(heap pollution)을 야기 할 수 있는 코드 때문에 생기는 경고 메세지를 말합니다.

즉, 이것도 설명은 긴데요.

앞서 보았던 타입 안정성에 대한 경고를 뜻합니다. 

public class HeapPollution {
    static String firstOfFirst(List<String>... strings) {
        List<Integer> ints = Collections.singletonList(42);
        Object[] objects = strings;
        objects[0] = ints;

        return strings[0].get(0);
    }
}

 

위 코드를 사용하면 나타는 경고는 아래와 같습니다.

 

완벽히 안전하다 생각이 된다면  @SafeVarargs 애노테이션을 붙여 경고를 끌 순있답니다. 

선언부는 생성자와 메서드 레벨입니다.😊

@SafeVarargs
static String firstOfFirst(List<String>... strings) {
    List<Integer> ints = Collections.singletonList(42);
    Object[] objects = strings;
    objects[0] = ints;

    return strings[0].get(0);
}

 

Tip) Varargs method 사용

저렇게 위험한 가변인수 메서드는 저장이 일어나지 않는 경우

또는 외부로 사용한 참조를 반환하지 않는 경우만 사용하라고 합니다.


결론

배열은 공변한다는 특징이 있는데요. 이것은 런타임에 타입이 수시로 변경될 수 있음을 의미하므로 배열보다는 리스트를!

또 new List<E>[], new E[], new List<Object> 등은 실사화 불가 타입임으로 불편함이 존재한다.

이럴 때도 앵간하면 List로 해결해라.

Varargs Method 역시 힙 오염을 발생 시킬 수 있으니 사용은 자제하고, List로 대체하자

 


참고자료

[1] Varargs method in java