[교재 EffectiveJava] 아이템 28. 배열보다는 리스트를 사용하라

반응형
728x90
반응형

배열과 제네릭 

배열과 제네릭 타입에는 중요한 차이가 두가지 있다.

 

배열은 공변이다.

공변이란, 함께 변한다는 뜻이다. Sub 가 Super 의 하위타입일 경우, 배열 Sub[] 는 배열 Super[] 의 하위 타입이 된다. 

 

제네릭은 불공변이다.

즉, 서로 다른 타입 Type1, Type2 가 있을때 List<Type1>,은 List<Type2> 의 하위 타입도 아니고 상위 타입도 아니다.  

 

 

배열 vs 리스트

1) 배열의 경우
Object[] objectArray = new Long[1];
objectArray[0] = "sss"; // 런타임시에 에러가 발생

 

배열은 런타임 시점에 오류를 알 수 있다.

 

2) 리스트의 경우
List<Object> ol = new ArrayList<Long>();
ol.add("sss");

 

리스트는 컴파일할 때 바로 알 수 있다.  오류를 컴파일 시에 알아채는 쪽을 더 선호할 것이다. 

 

 

배열과 제네릭 

배열은 실체화(reify) 된다. 실체화가 된다는 것은 런타임에 타입 정보를 알 수 있다는 것이다.

 

배열은 런타임에도 자신이 담기로 한 원소의 타입을 인지하고 확인한다. 그래서 Long 배열에 String 을 넣으려고 하면 ArrayStoreException 이 발생한다.

 

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

 

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

 

배열과 제네릭은 잘 어우러지지 못한다. 배열은 제네릭 타입, 매개변수화 타입, 타입 매개변수로 사용할 수 없다. 즉 코드를 new List<E>[], new List<String>[], new E[] 이런 식으로 작성하면 컴파일할 때 제네릭 배열 생성 오류를 일으킨다.

 

 

제네릭 배열의 생성을 막은 이유

제네릭 배열은 타입이 안전하지 않다. 이를 허용하면 컴파일러가 자동 생성한 형변환 코드에서 런타임에 ClassCastException 이 발생할 수 있다. 이는 런타임에 ClassCastException 의 발생을 막겠다는 제네릭 타입 시스템의 취지에 어긋난다.

 

배열 제네릭이 가능하다고 가정하기
List<String>[] stringLists = new List<String>[1]; // 만약 허용이 된다면? 
List<Integer> intList = List.of(31);
Object[] objects = stringLists; // List<String> 타입의 배열을 Object 배열에 할당 
objects[0] = intList; // List<Integer> 인스턴스를 Object 배열의 첫 원소로 저장 
String s = stringLists[0].get(0);

 

런타임에는 List<Integer> 인스턴스의 타입은 단순히 List 가 되고, List<Integer>[] 의 타입은 List[] 가 된다. 따라서 objects 배열에 List<Integer> 인스턴스를 넣어도 ArrayStoreException 이 발생되지 않는다.

 

List<String> 인스턴스만 담겠다고 선언한 stringLists 에 문제가 있다. 배열에는 지금 List<Integer> 인스턴스가 저장되어있다. 그리고 마지막 코드에서는 stringLists 배열의 처음 리스트에서 첫 원소를 꺼내려고 한다. 이때 컴파일러는 꺼낸 원소를 자동으로 String 으로 형변환 하는데, 해당 원소는 Integer 이므로 런타임에 classCastException 이 발생한다.

 

이런 일을 방지하려면 제네릭 배열이 생성되지 않도록 컴파일 오류를 발생시켜야한다.

 

 

실체화 불가 타입

E, List<E>, List<String> 같은 타입을 실체화 불가 타입(non-reifiable type) 이라고 한다. 쉽게 말해, 실체화되지 않아서 런타임에는 컴파일 타임보다 타입 정보를 적게 가지는 타입이다. 소거 메커니즘에 의해, 매개변수화 타입 가운데 실체화할 수 있는 타입은 List<?>, Map<?, ?> 같은 비한정적 와일드카드 타입 뿐이다. 배열을 비한정적 와일드카드 타입으로 만들 수는 있지만, 유용하게 쓰일 일은 거의 없다. 

 

 

제네릭으로 변환 과정

배열로 형변환할 때 제네릭 배열 생성 오류나 비검사 형변환 경고가 뜨는 경우 대부분은 배열인 E[] 대신 List<E> 를 사용하면 해결된다. 이렇게 사용하면 코드가 조금 복잡해지지만, 그 대신 타입 안정성과 상호운용성은 좋아진다.

 

제네릭 사용하지않은 버전
import java.util.Collection;
import java.util.Random;
import java.util.concurrent.ThreadLocalRandom;

public class Chooser {
    private final Object[] choiceArray;

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

    public Object choose() {
        Random rnd = ThreadLocalRandom.current(); 
        // Object type - 각 클라이언트에서 형변환 필요
        return choiceArray[rnd.nextInt(choiceArray.length)];
    }
}

 

제네릭을 쓰지 않은 코드로, 제네릭 적용이 시급한 코드다. choose 메서드를 호출할때마다 반환된 Object를 원하는 타입으로 형변환해야 한다. 

 

제네릭 사용 버전
package com.java.effective.item28;

import java.util.Collection;
import java.util.Random;
import java.util.concurrent.ThreadLocalRandom;

public class ChooserGeneric<T> {
    private final T[] choiceArray;

    public ChooserGeneric(Collection<T> choices) {
        // 경고 발생 : T가 무슨 타입인지 알 수 없으니 형변환이 런타임에도 안전한지 알 수 없다
        this.choiceArray = (T[]) choices.toArray();
    }

    public Object choose() {
        Random rnd = ThreadLocalRandom.current();
        // Object type - 각 클라이언트에서 형변환 필요
        return choiceArray[rnd.nextInt(choiceArray.length)];
    }
}

 

제네릭에서는 원소의 타입 정보가 소거되어 런타임에는 무슨 타입인지 알 수 없다. 해당 코드는 동작은 정상적으로 하며, 단지 컴파일러가 안전을 보장하지 못할 뿐이다.

 

제네릭 타입의 List 구현
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Random;
import java.util.concurrent.ThreadLocalRandom;

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

    public ChooserGenericList(Collection<T> choices) {
        // 경고 발생 : T가 무슨 타입인지 알 수 없으니 형변환이 런타임에도 안전한지 알 수 없다
        this.choiceList = new ArrayList<>(choices);
    }

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

 

해당 코드는 코드양이 많아지고 조금 느릴테지만, 런타임에 ClassCastException 을 만날 일은 없으니 그만한 가치가 있다.

 

 

반응형

Designed by JB FACTORY