티스토리 뷰

728x90
반응형

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


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

 

💡 차이점 1) 배열은 공변이고, 제네릭은 불공변입니다.

  • 배열은 공변입니다. 공변이란 예를들어 Sub가 Super의 하위 타입이라면 배열 Sub[]는 배열 Super[]의 하위 타입입니다.(공변 = 함께 변한다.)
  • 반면 제네릭은 불공변입니다. 즉 서로 다른 타입 Type1, Type2가 있을 때 List<Type1>은 List<Type2>의 하위 타입도 상위 타입도 아닙니다. 그냥 서로 다른 타입입니다.
  • 하지만 문제가 되는 것은 배열입니다.

🧨 예제 코드

  • Case 1같은 경우는 문법적으로는 허용되며 컴파일 단계에서 문제를 발견하지 못하고 런타임시에 예외가 발생합니다.
  • Case 2같은 경우는 컴파일 에러가 발생하여 개발자가 바로 알 수 있습니다.
// Case 1
Object[] objects = new Long[1];
objects[0] = "문자열을 넣을 수 없습니다."; // ArrayStoreException 발생(런타입 에러)
        
// Case 2
List<Object> list = new ArrayList<Long>(); // 컴파일 에러 발생
list.add("문자열을 넣을 수 없습니다.");

 

💡 차이점 2) 배열은 실체화, 제네릭은 소거

  • 배열은 실체화가 가능합니다. 무슨 뜻이냐면, 배열은 런타임에도 자신이 담기로 한 원소의 타입을 인지하고 확인합니다. 그래서 위의 예제에서 Long 배열에 String을 넣으려 할 때 담기로한 원소의 타입을 확인할 때 다른 걸 확인해서 예외를 발생시킵니다.
  • 하지만 제네릭은 타입 정보가 런타임에는 소거됩니다. 원소 타입을 컴파일 타임에만 검사하며 런타임에는 알 수 조차 없다는 의미입니다.

 

💡 배열은 제네릭을 사용할 수 없습니다.

  • 배열은 제네릭 타입, 매개변수화 타입, 타입 매개변수를 사용할 수 없습니다.
List<E>[], new List<String>[], new E[] // 컴파일 에러 발생

 

💡 제네릭은 배열을 만들지 못합니다.

  • 그 이유는 타입이 안전하지 않기 때문입니다. 이를 허용한다면 커파일러가 자동 생성한 형변환 코드에서 런타임에 ClassCastException이 발생할 수 있습니다.
  • 만약 아래의 코드에서 (1) 중 new List<String>[1]이라는 제네릭 배열이 허용된다면 아래와 같이 진행됩니다.
(1) - List<String>[] stringLists = new List<String>[1];
(2) - List<Integer> intList = List.of(42);
(3) - Object[] objects = stringLists;
(4) - objects[0] = intList;
(5) - String s = stringLists[0].get(0);
  1. (1)에서 stringLists라는 List<String> 타입의 제네릭 배열이 생성됩니다.
  2. (2)에서 42라는 원소를 가진 Integer 타입 매개변수의 intList가 선언됩니다.
  3. (3)에서 stringLists 변수를 Object 배열인 objects에 할당합니다.
  4. (4)에서 intList를 Object 배열의 0번 index에 할당합니다. 런타임시 List<Integer>는 로 타입인 List가 되고, List<Integer>[]는 List[]가 되기에 정상적으로 objects에 들어가며 ArrayStoreException이 발생하지 않습니다.
  5. (5)에서 stringLists라는 제네릭 배열의 0번째 인덱스에 있는 List를 꺼내서 get 으로 0번째 값을 꺼내려고 할 때 문제가 발생합니다.
    1. 컴파일러는 꺼낸 원소를 자동으로 String으로 형변환하는데 실제 원소는 Integer 타입이기 때문에 런타임시 ClassCastException이 발생합니다. 즉 제네릭의 장점인 타입 안전성이 무용지물 됩니다.

 

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

  • 실체화 불가 타입이란 실체화가 되지 않아서 런타임에는 컴파일타임보다 타입 정보를 적게 가지는 타입입니다. 소거 매커니즘으로 인해 매개변수화 타입 중에서 실체화가 가능한 타입은 List<?>, 또는 Map<?, ?>와 같은 비한정적 와일드카드 타입뿐입니다.
  • 아래는 실체화 불가 타입입니다.
  • 정규 타입 매개변수 E
  • 제네릭 타입 List<E>
  • 매개변수화 타입 List<String>

 

💡 제네릭 타입과 가변인수 메서드(varargs method)

  • 제네릭과 가변인수 메서드를 같이 사용하면 경고 메세지가 출력되는데, 가변인수 메서드를 호출할 때마다 가변인수 매개변수를 담을 배열이 만들어지는데 이때 이 배열의 원소가 실체화 불가 타입이라면 경고가 발생하게 됩니다. 그리고 이러한 경고는 @SafeVarags라는 어노테이션으로 대체할 수 있습니다.

 

💡 예제 코드

  • 배열로 형변환시 제네릭 배열 생성 오류로, 비검사 형변환 경고같은 대부분의 문제들은 배열(E[]) 대신 컬렉션(List<E>)로 대체할 수 있습니다. 컬렌션을 사용함으로써 코드가 복잡해지고 성능이 떨어질 수 있지만 대신 타입 안정성과 상호 운용성측에서 이점을 가질 수 있습니다.
  • 제네릭을 사용하지 않는 Object 객체 배열을 사용한 코드입니다. 이 코드는 choose 메서드에서 호출할 때마다 반환되는 타입은 Object 타입이기 때문에 원하는 타입으로 형변환을 해줘야 합니다. 그리고 내가 변환하고자 하는 타입과 다를 경우 형변환 오류가 발생합니다.
public class Chooser {

    private final Object[] choiceArray;

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

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

 

  • 아래 처럼하면 어떠한 경고도 없이 컴파일이 됩니다. 물론 코드의 양이 늘고 조금은 복잡해지고 속도도 조금 떨어질 수 있지만 타입안정성을 확보할 수 있습니다.
public class ChooserV3<T> {
    
    private final List<T> choiceList;

    public ChooserV3(Collection<T> choiceList) {
        this.choiceList = new ArrayList<>(choiceList);
    }

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

 

💡 정리

  • 배열은 공변이고 실체화되기 때문에 런타임시에는 타입 안전하지만 컴파일시에는 불안전합니다.
  • 제네릭은 불공변이고 타입 정보가 런타임시에 소거되므로 런타임시에는 불안전하지만 컴파일시에는 안전합니다.
  • 둘을 섞어쓰면 서로의 장점이 사라지기 때문에 따로 사용하는게 좋습니다.
  • 컴파일 오류나 경고를 만나면 배열을 리스트로 대체하는 방법을 고려해보고 적용해보는게 좋습니다.

 

이번 아이템은 어려운 부분이 많아서 따로 학습이 필요할 것 같습니다..!

 

 

참고자료)

https://catsbi.oopy.io/83635cfe-1cab-43f2-a943-56a9efd83fb2

 

 

 

 

 

728x90
반응형