관리 메뉴

bright jazz music

[effective java 3rd - ch.05 제네릭] Item28 : 배열보다는 리스트를 사용하라 본문

JAVA/effective java 3rd edition

[effective java 3rd - ch.05 제네릭] Item28 : 배열보다는 리스트를 사용하라

bright jazz music 2022. 11. 25. 14:22

배열과 제네릭 타입의 차이

배열

  • 배열은 공변(covariant, 함께 변함)한다.
  • Sub가 Super의 하위타입이라면 배열 Sub[]는 배열 Super[]의 하위타입이 된다.

 

제네릭

  • 제네릭은 공변하지 않는다.
  • 서로 다른 타입 Type1과 Type2가 있을 때, List<Type1>은 List<Type2>의 하위 타입도 아니고 상위 타입도 아니다.

 

리스트를 사용해야 하는 이유 1 : 런타임이 아닌 컴파일 시에 오류를 검출하기 위해

배열을 사용했을 때 : 런타임이 되어서야 오류가 발생함.

public class Test {

    public static void main(String[] args){

     Object[] objectArray = new Long[1];
     objectArray[0] = "타입이 달라 넣을 수 없다";
     //런타임에 ArrayStoreException 발생
        //IDE에서 아래와 같이 경고: 스트링 타입을 롱 타입의 배열에 넣으면 ArrayStoreException이 발생할 거라고 경고한다.
        //Storing element of type 'java.lang.String' to array of 'java.lang.Long' elements will produce 'ArrayStoreException'
    }
}

 

리스트를 이용했을 때 : 컴파일에 실패함.

public class Test {

    public static void main(String[] args){

        List<Object> objectList = new ArrayList<Long>(); //호환되지 않는 타입
        //java: incompatible types: java.util.ArrayList<java.lang.Long> cannot be converted to java.util.List<java.lang.Object>
        objectList.add("타입이 달라 넣을 수 없다");
    }
}

 

어느 쪽이든 Long용 저장소에 String을 넣을 수는 없다. 다만 배열에서는 그 문제를 런타임에야 알게 된다. 리스트를 사용하면 컴파일 시에 알게 된다.

 

 

 

리스트를 사용해야 하는 이유 2 : 배열에는  제네릭을 사용할 수 없다

제네릭 배열을 사용할 수 없는 이유

  • 제네릭 배열은 만들 수 없다. 만들지 못하도록 막아놓았다.
  • new List<E>[], new List<String>[], new E[]와 같은 방식으로 코드를 작성하면 제네릭 배열 생성 오류가 발생한다.
  • 만들지 못하도록 막은 이유는 타입 안전성이 안전하지 않기 때문이다.

 

제네릭 배열이 안전하지 않은 이유

  • 제네릭 배열을 허용한다면 컴파일러가 자동 생성한 형변환(Casting) 코드에서 런타임에 ClassCastException이 발생할 수 있다.
  • 이는 런타임에 ClassCastException이 발생하지 않도록 하겠다는 제네릭 타입 시스템의 취지에 어긋나는 것이다.

 

제네릭 배열이 런타임 시에 ClassCastException을 발생시킬 수 있는 이유

  • 제네릭이 실체화(reify)되기 때문이다.
  • 즉, 배열은 런타임에도 자신이 담기로 한 원소의 타입을 인지하고 확인한다.
  • 예제에서 제네릭이 아닌 일반 배열에서도 Long 배열에 String을 넣으려 했을 때 ArrayStoreException이 발생함을 확인하였다.
  • 제네릭에서는 그런 일이 일어나지 않는다. 제네릭은 타입 정보가 런타임에는 소거된다.
  • 즉, 제네릭에서는 원소의 타입을 컴파일 시에만 검사하며 런타임 시에는 알지 못한다.
  • 제네릭에서 런타임 시에 원소 타입을 소거시키는 이유는 제네릭 이전의 레거시 코드와의 호환성을 위해서이다.

 

예시

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)는 원소가 하나인 List<Integer>를 생성한다.
(3)은 (1)에서 생성한 List<String>의 배열을 Object 배열에 할당한다. 배열은 공변이니 문제 없다.
(4)는 (2)에서 생성한 List<Integer>의 인스턴스를 Object 배열의 첫 원소로 저장한다. 
제네릭은 소거 방식으로 구현되어 있어서 문제 없다.

즉 런타임에는 List<Integer> 인스턴스의 타입은 단순히 List가 되고, List<Integer>[] 인스턴스의 타입은 List<[]가 된다.
따라서 (4)에서도 ArrayStoreException을 일으키지 않는다.

이제부터 문제가 발생한다.
List<String> 인스턴스만 담겠다고 선언한 stringLists 배열에는 현재 List<Integer> 인스턴스가 저장돼 있다.

(5)는 이 배열의 처음 리스트에서 첫 원소를 꺼내려한다. 컴파일러는 꺼낸 원소를 자동으로 String으로 형변환하는데,
이 원소는 Integer이므로 런타임에 ClassCastException이 발생한다.

이런 일을 방지하려면 제네릭 배열이 생성되지 않도록 (1)에서 오류를 내야 한다.
*/


//즉, List<String>[] stringLists 배열의 첫 번째 원소가 List<Integer> intList 가 되어버린 것.
//따라서 처음부터 제네릭 배열은 만들어져서는 안되는 것이다.

 

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

실체화 되지 않아서 런타임에는 컴파일 타임보다 타입 정보를 적게 가지는 타입이다.

 

실체화 불가 타입의 예

  • E
  • List<E>
  • List<String>
  • etc.

실제화 가능 타입은 비한정적 와일드카드뿐이다.(소거 매커니즘 때문)

  • List<?>
  • Map<?>

그러나 유용하게 쓰일 일이 없다.

 

 

 

제네릭 컬렉션에서는 자신의 원소 타입을 담은 배영을 반환하는 게 보통은 불가능하다. 이로서 발생하는 번거로움의 대부분을 해결하는 방법이 존재하기는 한다(item33). 또한 제네릭 타입과 가변인수 메서드(varargs method)를 함께 사용하면 경고메시지가 발생한다.

 

==> 가변인수 메서드를 호출할 때마다 가변인수 매개변수를 담을 배열이 하나 만들어지는데, 이 때 그 배열의 원소가 실체화 불가 타입이라면 경고가 발생한다. 이 문제는 @SareVarargs 어노테이션으로 대처할 수 있다.

 

배열로 형변환 하는 경우

배열로 형변환 할 때 제네릭 배열 생성 오류나 비검사 형변화 경고가 뜨는 경우,  배열 E[] 대신 컬렉션인 List<E>를 사용하면 해결된다. 코드가 복잡해지고 성능이 저하될 수 있지만 타입 안정성과 상호운용성에서 오는 이점이 더 크다.

 

 

예)

Chooser.java

cf. toArray() : http://asuraiv.blogspot.com/2015/06/java-list-toarray.html

public class Chooser{
	private final Object[] choiceArray; //Object 타입 객체를 원소로 갖는 배열 생성
    
    public Chooser(Collection choices){	//생성자
    	choiceArray = choices.toArray(); //toArray() 컬렉션을 배열로 만들어줌.
    }
    
    public Object choose(){
    	Random rnd = ThreadLocalRandom.current(); //난수 생성
        
        return choiceArray[rnd.nextInt(choiceArray.length)];
        //Random.nextInt() 난수 생성범위 지정
    }

}

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

 

이 클래스를 제네릭으로 만들어 보자.

 

 

제네릭 클래스 Chooser.java

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

/*컴파일 시 **에러 **발생
error: incompatible types: Object[] cannot be converted to T[]
	choiceArray = choices.toArray();
    
    where T is a type-variable:
    T extends Object declared in class Chooser
*/

위의 코드는 형변환 에러가 발생한다. 따라서 Object 배열을 T 배열로 형변환하면 된다.

 

public class Choose<T> {
	private final T[] choiceArray;
    
    public Chooser(Collection<T> choices) {
    	choiceArray = (T[]) choice.toArray();	//T[] 배열로 캐스팅
    }
    
    public Object choose(){
    	Random rnd = ThreadLocalRandom.current();
        
        return choiceArray[rnd.nextInt(choiceArray.length)];
    }
}

// ** 경고 발생 **
/*
warning: [unchecked] unchecked cast
	choiceArray = (T[]) choices.toArray();

required: T[], found: Object[]
where T is a type-variable:
	T extends Object declared in class Chooser
  
*/

위의 경고는 T의 타입을 알 수 없으니 컴파일러는 이 형변환이 런타임에도 안전한지 보장할 수 없다는 의미이다.

 

제네릭은 런타임 시 원소의 타입 정보가 소거된다. 이 프로그램은 동작하지만 컴파일러가 안전을 보장하지 못할 뿐이다. 따라서 작성자가 이상이 없다고 판단한다면 주석을 달고 @SuppressWarning() 어노테이션을 달아 경고를 숨겨도 된다. 하지만 이왕이면 경고의 원인을 제거하는 것이 좋다.

 

리스트를 써라

비검사 형변환 경고를 제거하려면 배열 대신 리스트를 쓰면 된다. 아래의 Chooser는 오류나 경고 없이 컴파일 된다.

 

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

 

---

 

핵심 정리

  • 배열과 제네릭에는 다른 타입 규칙이 적용된다.
  • 배열은 공변이고 실체화가 된다.
  • 제네릭은 불공변이고 타입 정보가 소거된다.
  • 배열은 런타임에는 타입 안전하지만 컴파일에는 불안전하다.
  • 제네릭은 런타임에는 불안전하지만 컴파일에는 안전하다.
  • 따라서 둘을 섞어 쓰기가 어렵다.
  • 혼합하여 사용 시 컴파일 오류나 경고를 마주하면 가장 먼저 배열을 리스트로 대체하는 방법을 적용해 보자.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Comments