Home Item 28 - 배열보다는 리스트를 사용하라
Post
Cancel

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

배열과 리스트(제너릭 타입)의 차이

1. 배열 (Array)

1) 배열은 함께 변한다. 즉, 공변(convariant)이다.

  • Sub extends Super인 경우, Sub[] extends Super[]이다.
1
2
3
4
class Sub extends Super {
}

Super[] superman = new Sub[1];

2) 배열은 실체화(reify)된다.

  • 컴파일타임: 타입 불안전 / 런타임: 타입 안전
    1
    2
    
    Object[] objectArray = new Long[1];
    objectArray[0] = "String을 넣어봅시다!";
    

    Object 배열에 Long용 저장소를 할당했다.
    그러므로 objectArray에는 Long타입만 올 수 있다. String은 못온다.
    그런데 위 코드는 컴파일 에러가 나지 않는다.

    1
    2
    
    Exception in thread "main" java.lang.ArrayStoreException: java.lang.String
    at item28.BetweenArrayAndList.main(BetweenArrayAndList.java:7)
    

    런타임에서 에러가 난다.
    → 런타임에도 자신이 담기로 한 원소의 타입을 인지하고 확인하기 때문

2. 리스트 (List)

1) 리스트는 함께 변하지 않는다. 즉, 불공변(invariant)이다.

  • 상위-하위타입 관계가 아니다.
    1
    
    List<Super> superList = new ArrayList<Sub>();//컴파일 에러!
    

2) List는 실체화(reify)되지 않는다.

  • 컴파일타임: 타입 안전 / 런타임: 타입 불안전
    1
    2
    3
    
    //컴파일 에러
    List<Object> objList = new ArrayList<Long>();
    objList.add("String을 넣어봅시다!");
    

    리스트는 컴파일할 때 바로 알 수 있다. 단, 런타임 시 타입정보가 소거된다.
    원소 타입을 컴파일 타임에만 검사, 런타임에는 알 수 조차 없다.

→ 소거: 제너릭이 지원되기 전의 레거시 코드와 제너릭 타입을 함께 사용할 수 있게 해주는 메커니즘
자바5가 제너릭으로 순조롭게 전환될 수 있도록 해준 일등공신!

실체화(reify)

  • 실체화: JVM이 type 정보를 완전히 알게 되거나, 알게 하는 것, 또는 알고 체크하는 것
  • 실체화 가능 타입(reifiable type)
    • 매개변수화 타입 가운데 실체화 될 수 있는 타입은 비한정적 와일드카드 뿐이다.
    • ex) List<?>, Map<?,?>
  • 실체화 불가 타입(non-reifiable type)
    • 실체화되지 않아서 런타임에는 컴파일타임보다 타입 정보를 적게 가진다.
    • ex) E, List<E>, List<String>

제너릭 배열을 만들지 못하게 막은 이유는?

1. 배열과 제너릭은 잘 어우러지지 못한다.

new List<E>[], new List<String>[], new E[] → 컴파일 시 오류 발생!

2. 타입 안전하지 않다.

위 두가지 이유 때문인데 자세한건 아래 예시를 보고 이해해보자.
ex) 제너릭 배열 생성이 허용된다 가정

1
2
3
4
5
List<String>[] stringList = new List<String>[1]; // (1) 허용된다 가정
List<Integer> initList = List.of(42);            // (2) 
Object[] objects = stringList;                   // (3)
objects[0] = initList;                           // (4)
String s = stringList[0].get(0);                 // (5)

(1)처럼 제너릭 배열이 생성이 허용된다고 일단 가정해보자.
(2)에서 원소가 42 하나인 List<Integer>를 생성한다.
(3)은 (1) 에서 생성한 List<String> 배열을 Object 배열에 할당한다. 즉, Object 배열은 List<String> 배열을 참조하게 된다. 배열은 공변이므로 가능하다.
item28_If_there_is_generic_array (4)는 Objects 배열의 첫 원소에 원소가 42 하나인 initList를 저장한다.

  • 문제점
    • List<String> 인스턴스만 받겠다고 선언한 stringList 배열에 List<Integer> 인스턴스가 들어가있다.

(5)는 이 배열의 첫 리스트에서 원소를 꺼내려 한다. 컴파일러는 꺼낸 원소 42를 자동으로 String으로 형변환한다.

1
String s = (String) 42; // ClassCastException 발생

이런 일을 방지하기 위해서 제너릭배열이 생성되지 않도록 (1)에서 컴파일 오류를 내야하는 것이다.

배열대신 List처럼 원소를 관리하자

배열로 형변환할 때 제너릭 배열 생성오류 or 비검사 형변환 경고가 뜨는 경우
→ 배열인 E[] 대신 컬렉션인 List<E>를 사용하자!

ex) 생성자에서 컬렉션을 받는 Chooser 클래스
이 클래스는 컬렉션 안의 원소 중 하나를 무작위로 반환하는 choose 메서드를 제공한다.

제너릭 미사용 버전

1
2
3
4
5
6
7
8
9
10
11
12
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)];
    }
}

이 클래스를 사용하려면 choose 메서드 호출 시 마다 반환된 Object를 원하는 타입으로 형변환 해야한다.
혹시나 원하는 타입과 다른 타입의 원소가 들어있는 경우 런타임 시 형변환 오류가 발생할 것이다.

제너릭 사용 버전 1

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Chooser2<T> {//제너릭으로 변환
    private final T[] choiceArray;

  //@SuppressWarnings("unchecked") // 비검사 형변환 경고(unchecked cast경고)
    public Chooser2(Collection<T> choices){
        choiceArray = (T[]) choices.toArray(); //unchecked cast: 'java.lang.Object[]' to 'T[]'
    }

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

choiceArray = (T[]) choices.toArray();에서 unchecked cast: ‘java.lang.Object[]’ to ‘T[]’ 경고가 발생한다.

  • T[]가 무슨 타입인지 알 수 없으니 컴파일러는 이 형변환이 런타임에도 안전한지 보장할 수 없다라는 뜻
  • 제너릭에는 원소의 타입 정보가 소거되어 런타임에는 무슨 타입인지 알 수 없다.
  • 컴파일러가 안전을 보장하지 못할 뿐 소스코드는 동작하며, List에 비해 빠르다.
  • 코드작성자가 안전하다고 확신한다면 주석을 남기고 애너테이션을 달아 경고를 숨겨도 된다.
    • 단, 경고의 원인을 완전히 제거하는게 훨씬 좋은 방법이다(item 27)

제너릭 사용 버전 2

1
2
3
4
5
6
7
8
9
10
11
12
public class Chooser3<T> {//제너릭으로 변환
    private final List<T> choiceList;//리스트로 변경

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

    public Object choose(){
        Random rnd = ThreadLocalRandom.current();
        return  choiceList.get(rnd.nextInt(choiceList.size()));
    }
}
  • 장점: 타입 안전성과 상호운용성이 좋아진다. 런타임이 아닌 컴파일시점에 에러를 잡아준다.
  • 단점: 코드가 조금 복잡해진다. 성능이 살짝 느려질 수 있다.

결론

  • 배열과 리스트를 섞어쓰기란 쉽지 않다.
    • Array: 공변. 실체화된다.
      • 컴파일타임: 타입 불안전 / 런타임: 타입 안전
    • List: 불공변. 타입정보가 소거된다.
      • 컴파일타임: 타입 안전 / 런타임: 타입 불안전
  • 섞어쓰다가 컴파일 오류나 경고를 만나면, 배열을 List로 대체해라.

참고

This post is written by PRO.