티스토리 뷰

Java

'generic array creation' compile error

LichKing 2019. 9. 27. 15:37

generic 을 이용한 배열 객체를 생성할때는 컴파일단에서 에러가 발생하게된다.

List<String>[] lists = new ArrayList<String>[10];

ArrayList 의 배열을 만들고자하는 코드다. 대충 쉽게 이해하면 일종의 2차원 배열이라고 볼 수 있다. 하지만 이 코드는 'generic array creation' 이라는 컴파일 에러와 함께 컴파일 되지않는다. 왜 이럴까?

 

먼저 제네릭이 추구하는 바가 뭔지를 알아보고, 배열과 제네릭 리스트의 차이를 이해해야 컴파일러가 저걸 왜 막는지 이해할 수 있다.

제네릭이 추구하는 바는 타입 안정성이다. 제네릭이 사용된 코드는 타입 안정성을 보장해야한다. 타입 안정성이 보장될 수 없다면 제네릭은 사용될 이유가 없다. 그런데 배열과 제네릭의 차이점으로 인해 제네릭 배열은 타입 안정성을 보장할 수 없다. 이제 그 차이를 알아보자.

 

배열은 공변이고, 제네릭은 불공변이라는 말은 제네릭을 조금 공부해보면 숱하게 듣게되는 이유다. 그리고 'generic array creation' 에러 역시 이로인해 발생하는 점이다. 아래 코드가 어떻게 작동할지 생각해보자.

 

public static void main(String[] args) {             
    List<String>[] lists = new ArrayList[10];        
                                                     
    List<Integer> numbers = new ArrayList<>();       
    numbers.add(1);                                  
    Object[] objects = lists;                        
    objects[0] = numbers;                            
                                                     
    System.out.println(lists[0].get(0));             
}                                                    

List<String>[] 타입에 ArrayList<String>[10] 인스턴스는 'generic array creation' 에러로 인해 할당할 수 없다. 그래서 예제코드 자체를 작성할 수 없기때문에 이번 예제코드는 ArrayList<String>[10] 이 아니라 ArrayList[10] 으로 작성했다. raw 타입을 사용하면 타입 안정성을 컴파일러가 잡을 필요없다고 명시하는 것과 같다.(컴파일은 되지만 워닝 메세지를 띄우게된다.) 물론 예제코드가 아닌 코드에서는 이런 raw 타입은 쓰지않는게 좋다.

 

Item 26. 로 타입은 사용하지 말라

 

일단 예제를 실행하기위해 raw 타입으로 ArrayList의 배열을 만들어 lists 변수에 할당했다. 그리고 별도의 List<Integer> 변수를 만들었다. 그 아래줄에서는 Object[] 배열 변수를 하나 만들어 제일 처음 만든 lists 변수를 할당했다. 배열은 공변 타입이기때문에 모든 배열 타입은 Object[] 타입의 하위 타입이 된다. 그래서 Object[] 타입에 List[] 변수를 업캐스팅하는게 가능하다. 이제 objects 변수는 object[] 타입이 됐기때문에 List에 <String> 으로 지정된 제네릭 타입은 신경쓰지않는다. 그래서 objects 배열의 0 인덱스에 List<Integer> 타입을 집어넣는게 가능하다. 여기까지는 컴파일 에러는 물론이고, 런타임 에러도 발생하지않는다. 하지만 가장 아래에서 출력하는 구문에서 타입 캐스팅이 발생하게되고 이때 Integer 1은 String으로 변환되지못해 런타임 예외가 발생하게 된다.

 

이 코드는 결국 예외를 맞이하며 중단되지만 샘플 코드의 경우 컴파일러가 타입 안정성을 보장하지못한다고 경고를 하고있다. 다만 이 코드에서 만약 new ArrayList<String>[10] 이 가능했다고 하더라도 공변인 배열과 뒤섞여 발생하는 이 런타임 예외를 막을길이 없다. 저게 허용될 경우 제네릭을 사용해서 타입 안정성이 보장됐을거라고 생각한채로 컴파일러의 경고조차 없는 상태에서 런타임 예외를 맞게되는것이다.

 

정리하면 제네릭은 타입 안정성을 위해 존재하는데 제네릭 배열의 경우 제네릭의 특성상 타입 안정성을 애초에 보장하지못하기때문에 아예 사용을 못하도록 막은것이다.

 

참고: 이펙티브자바

 

이펙티브 자바 3/E

자바 6 출시 직후 출간된 『이펙티브 자바 2판』 이후...

www.kyobobook.co.kr

부록

예제코드를 살짝 변경해봤다.(사실 처음 작성한 예제코드가 아래코드인데 문제가 좀 생겨서 위 코드로 변경된것이다.)

public static void main(String[] args) {         
    List<String>[] lists = new ArrayList[10];    
                                                 
    List<Integer> numbers = List.of(1, 2, 3);    
    Object[] objects = lists;                    
    objects[0] = numbers;                        
                                                 
    System.out.println(lists[0].get(0));         
}                                                

numbers 를 만드는 부분이 수정됐다. ArrayList 를 만들어서 add() 를 호출하던 부분이 JDK 9 부터 제공되는 List.of() 팩토리 메서드로 변경됐다. JDK 9 미만이라면 Arrays.asList() 를 써도 상관없다.

 

이 코드는 실행하게되면 println() 까지 오기전에 6 라인 objects[0] = numbers 에서 예외가 발생한다. 사실 생각해보면 되게 당연한건데 의외로 왜 발생하는지 헷갈리게된다.(나만 그럴수도있다)

 

예외가 발생하는 이유는 objects 는 lists 가 할당됐고, lists 는 ArrayList[] 이기때문이다. List.of() 혹은 Arrays.asList() 가 리턴하는 List 구현체는 java.util.ArrayList 가 아닌 다른 구현체이기때문에 ArrayList 배열에 집어넣을 수 없다.

댓글
댓글쓰기 폼