티스토리 뷰

반응형

배열과 제네릭의 차이

1) 배열은 공변(함께 변한다)이다. → Sub가 Super의 하위타입이면 Sub[]는 Super[]의 하위타입이 된다.
제네릭은 불공변이다. → 서로 다른 Type1, Type2 가 있으면 List은 List의 하위타입도 상위타입도 아니다.

//배열 : 런타임 오류 -> 문제가 있다!
Object[]objectArray = new Long[1];
objectArray[0] = "타입이 달라 넣을 수 없다."; // ArrayStoreException을 던진다.
//제네릭 : 컴파일 오류
List<Object> ol = new ArrayList<Long>(); // 호환되지 않는 타입이다.
ol.add("타입이 달라 넣을 수 없다.");

2) 배열은 실체화된다.

  • 배열은 런타임에도 자신이 담기로 한 원소의 타입을 인지하고 확인함
  • 반면, 제네릭은 타입 정보가 런타임에는 소거됨 → 컴파일 타임에만 검사, 소거는 제네릭 지원 전의 코드와 어우러지도록 하기 위한 메커니즘

 

 

배열과 제네릭의 부조화

  • 배열은 제네릭 타입(new List[]), 매개변수화 타입(new List[]), 타입 매개변수(new E[])로 사용할 수 없다. → 제네릭 배열 생성 오류
  • 제네릭 배열을 만들지 못하게 한 이유 : 타입 안전하지 않음 (컴파일러가 자동 생성한 형변환 코드에서 런타임에 ClassCastException 이 발생할 수 있다. 런타임에 이 예외가 발생하지 않도록 하는 것이 제네릭 타입 취지기때문에 모순임)
List<String>[] stringLists = new List<String>[1];  //(1)
List<Integer> intList = List.of(42);   //(2)
Object[] objects = stringLists;   //(3)
objects[0] = intList;  //(4)
String s = stringLists[0].get(0);   //(5)

위 코드의 (1)과 같은 제네릭 배열 생성이 가능하다면?

(2)에서는 원소가 하나인 리스트를 생성하고, (3)에서는 배열은 공변이므로 할당이 된다.

(4)에서는 제네릭은 소거방식이므로 런타임 시에 List는 List가 되고, List[]는 List[]가 된다. 그러므로 ArrayStoreException이 발생하지 않고 지나간다.

(5)에서가 문제다. 현재 stringLists의 0번째에는 List 인스턴스가 들어가있다.

꺼낸 원소를 Integer → String 으로 형변환을 하지 못해서 런타임에 ClassCastException 이 발생한다.

이런 런타임 시에 예외 발생을 막기 위해 제네릭 배열 생성을 막아서 컴파일 오류를 내야함

 

 

실체화 불가 타입(non-reifiable type)

  • E, List, List 같은 타입 → 런타임에 컴파일 타임보다 타입 정보를 적게 가짐.
  • 소거 매커니즘 때문에 List<?> 나 Map 같은 비한정적 와일드카드 타입만 실체화 가능
  • 제네릭 타입과 가변인수 메서드(varargs method)를 함께 쓰면 경고를 받게 된다. (실체화 불가 타입이므로)
    → @SafeVarargs 어노테이션으로 대처 가능
  • 배열로 형변환할때 제네릭 배열 생성오류나 비검사 형변환 경고가 뜨면 컬렉션인 List를 사용하자.
public class Chooser {
    private final Object[] choiceArray;

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

    public Object choose() { //메서드 호출 시 반환된 Object를 원하는 타입으로 형변환 필요
        Random rnd = ThreadLocalRandom.current();
        return choiceArray[rnd.nextInt(choiceArray.length)];
    }
}

위 코드의 choose() 메서드를 호출할때마다 반환된 Object를 원하는 타입으로 형변환해야하고, 만약 타입이 다른 원소가 들어있었다면 런타임에 형변환 오류가 난다. → 제네릭 타입으로 변환

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

    public Chooser(Collection<T> choices) {
        //toArray()를 Object[] 타입으로 형변환 할수없어서 컴파일 오류
        choiceArray = choices.toArray();
    }
}
public class Chooser<T> {
    private final T[] choiceArray;

    public Chooser(Collection<T> choices) {
        choiceArray = (T[]) choices.toArray();  //경고
    }
}

위 코드는 T가 무슨 타입인지 알 수 없어서 형변환이 런타임에도 안전하다고 보장할 수 없다고 경고가 뜬다.

제네릭은 런타임에 타입 정보가 소거되어 타입을 알 수 없어서 안전하진 못하다.

이 경고를 제거하기 위해서는 배열 대신 리스트를 쓰면 된다.

//배열 대신 리스트 사용 : 런타임 시 ClassCastException을 피한다.
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));
    }
}

 

결론

배열은 공변이고 실체화되지만, 제네릭은 불공변이고 타입 정보가 소거된다.

그래서 배열은 런타임에 타입 안전하지만, 컴파일 타임에는 그렇지 않다.

제네릭은 반대로 런타임에는 안전하지 않고, 컴파일 타임에는 안전하다.

그래서 둘을 섞어쓰기는 쉽지 않다 → 배열을 리스트로 대체하자.

반응형
댓글
반응형
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday