맞는데 왜 틀릴까..?

Java/Effective Java

[Effective Java] Item 28. 배열보다는 리스트를 사용하라

안도일 2023. 5. 9. 20:10

 

공변 vs 불공변

 

  • 공변 : Sub가 Super의 하위 타입이라면 Sub []도 Super []의 하위 타입이다.
  • 불공변 : Type1과 Type2가 있을 때 List <Type1>과 List <Type2>는 서로 하위 타입도 상위 타입도 아니다.

 

배열 : 공변

제네릭 : 불공변

 

배열은 공변성을 가지기 때문에 특정 타입의 하위 타입으로 변환될 수 있다.

ex) Object[] 배열은 String [] 배열로 변환될 수 있다.

따라서 컴파일타임에 타입 안전하지 않다. 

 

 

배열 vs 리스트 

 

1. 컴파일 시 오류 확인 가능

 

 

둘 다 Long용 저장소에 String을 넣을 수는 없지만 배열은 런타임 환경에서 에러를 던지고, 리스트는 컴파일할 때 바로 알 수 있다.

 

 

2. 배열은 실체화된다.

 

배열 : 런타임에도 자신이 담기로 한 원소의 타입을 인지하고 확인한다.

 

제네릭 : 타입 정보가 런타임에는 소거된다. 원소 타입을 컴파일 타임에만 검사하며 런타임에는 알 수 없다.

 

 

제네릭 배열 생성

 

이상의 차이로 인해 배열은 제네릭과 잘 어우러지지 못해서, 자바에서는 제네릭 배열을 생성하지 못하게 한다.

타입 안전하지 않기 때문이다.

 

 

제네릭 배열 생성을 허용하지 않는 이유

 

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.  제네릭 배열 생성 (1) 생성

 

2. 원소가 하나인 List <Integer> 생성

 

3. (1)에서 생성한 List <String>의 배열을 Object 배열에 할당 (배열은 공변이므로 가능)

 

4. (2)에서 생성한 List <Integer>의 인스턴스를 Object 배열의 첫 원소로 저장

 

(제네릭은 소거 방식으로 구현되어 런타임 환경에서)

List <Integer> 인스턴스의 타입 : List

List <Integer>[] 인스턴스의 타입 :  List []

 

5. List <String> 인스턴스만 담겠다고 선언한 stringLists 배열에 List <Integer> 인스턴스가 저장돼 있다.

 

6. (5) 이 배열에서 원소를 꺼내려할 때 컴파일러는 꺼낸 원소를 자동으로 String으로 형변환 하려 함.

 

7. 하지만 이 원소는 Integer이므로 런타임에 ClassCastException 발생 

 

배열 E [] 대신 컬렉션 List <E>를 쓰자

 

  • 배열로 형변환 할 때 제네릭 배열 생성 오류 또는 비검사 형변환 경고가 뜰 수 있다.
  • 이런 경우, 배열 E [] 대신 컬렉션 List <E>를 쓰자.

 

생성자에서 Collection을 받는 Chooser 클래스

Collection 안의 원소 중 하나를 무작위로 선택해 반환하는 choose를 제공한다.

 

제네릭을 사용하지 않은 방법

 

 

choose 메서드를 호출할 때마다 반환된 Object를 원하는 타입으로 형변환해야 한다.

따라서 형변환 하지 않은 경우 위처럼 타입 오류가 발생한다.

 

 

 

배열 기반 제네릭 Chooser

 

 

정상적으로 동작하긴 하지만 비검사 형변환 경고가 발생한다.

 

제네릭에서는 원소의 타입 정보가 소거되어 런타임에는 무슨 타입인지 알 수 없다.

따라서 T가 무슨 타입인지 알 수 없으니 컴파일러는 이 형변환이 런타임에도 안전한지 보장할 수 없다.

 

 

 

리스트 기반 제네릭 Chooser

 

 

배열 대신 리스트를 사용하여 Chooser 클래스를 완성시킬 수 있다.

코드의 양이 늘고 더 느릴 수 도 있지만 런타임에서 ClassCastException을 만날 일은 없으니 가치 있는 코드다.

 

 


 

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

그 결과 배열은 런타임에는 타입 안전하지만 컴파일타임에는 그렇지 않고, 제네릭은 그 반대다.

 

따라서 둘을 섞어 쓰긴 쉽지 않고, 둘을 섞어 쓰다가 오류가 발생하면 배열을 리스트로 대체하자.