배열은 공변이기 때문에 아래와 같은 배열 초기화는 가능하다.
배열은 Long 이 Object 의 하위 타입이기 때문에 Long[] 도 Object[] 의 하위 타입임이 성립된다.
Object[] array= new Long[1]; // Ok
array[0] = "런타임 에러 발생"; // java.lang.ArrayStoreException: java.lang.String
하지만 위와 같은 코드는 컴파일 에러를 발생시킬 수 있다.
반면 리스트는 불공변 이기 때문에 아래와 같은 리스트 초기화가는 불가하다.
리스트는 Long 이 Object 의 하위 타입이라도 List<Long> 과 List<Object> 는 상위 혹은 하위 타입의 관계로 바라볼 수 없다.
List<Object> list= new ArrayList<Long>(); // Nope 컴파일 에러 발생
배열, 리스트 모두 Long 타입으로 초기화한 저장소에 String 과 같은 다른 타입을 넣을 수 없다는 점은 동일하지만, 타입 안정성 측면에서 배열은 런타임에 진입해야 알 수 있고, 리스트는 컴파일 타임에 바로 인지 할 수 있다.
배열은 실체화 된다.
배열은 실체화(Reifiable)가 되는데(JLS 4.7), 앞서든 예시 처럼 Long 배열에 String 원소를 넣으면 ArrayStoreException 이 발생한다.
이는 런타임에도 담기로 한 원소의 타입을 인지하고 확인하기 때문이다.
반면 리스트는 타입 정보가 런타임에는 소거된다. (참조1, 참조2) 원소 타입을 컴파일 터임에만 검사하고, 런타임에는 알수 없다는 의미이다. 이는 제네릭 전 레거시 코드와 제네릭 타입을 함께 사용할 수 있게 해주는 메커니즘으로, Java 5 에서 이를 통해 순조로운 전환을 할 수 있었다.
public static <E> void printArray(E[] array) {
for (E element : array) {
System.out.printf("%s ", element);
}
}
// 컴파일 이후
public static void printArray(Object[] array) {
for (Object element : array) {
System.out.printf("%s ", element);
}
}
제네릭 배열은 생성할 수 없다.
위와 같은 이유들로 인해 제네릭배열(new List<E>[], new List<String>[], new E[])은 타입 안전하지 않기 때문에 생성할 수 없는데, 이는 아래와 같은 경우가 발생할 수 있기 때문이다.
List<String>[] stringLists = new List<String>[1]; // (1) 허용된다고 가정해보자.
List<Integer> intList = List.of(42); // (2) 원소가 하나인 List<Integer> 생성
Object[] objects = stringLists; // (3) stringLists를 objects에 할당
objects[0] = intList; // (4) intList를 objects의 첫번째 원소로 저장한다.
String s = stringLists[0].get(0); // (5) stringList[0]에 들어가있는 첫번째 요소는 Integer이므로 ClassCastException오류 발생.
컴파일러가 자동 생성한 형변환 코드에서 런타임 시점에 ClassCastException 이 발생할 수 있기 때문이다. 이는 ClassCastException 발생을 막겠다는 제네릭 타입의 취지와도 어긋나는 일이기도 하다. 이러한 이유로 (1) 시점에서 부터 컴파일 오류를 발생시킨다.
제네릭은 타입 소거로 인해 실체화 되지 않아서 런타임에는 컴파일타임보다 타입 정보를 적게 가지는 타입을 의미한다. 예외가 있다면 비한정 와일드 카드 타입(#26 ) 이다.
배열보다는 리스트를 사용하면 된다.
배열로 형변환할때 제네릭 배열 생성 오류나 비검사 형변환 경고가 뜨는 경우 대부분 배열인 E[] 대신, List<E> 를 사용하면 해결 가능하다.
public class Chooser {
private final Object[] choiceArray;
public Chooser(Collection choices) {
this.choiceArray = choices.toArray();
}
// 매번 형변환이 필요하기 때문에 형변환 오류가 발생할 수 있다.
public Object choose() {
Random rnd = ThreadLocalRandom.current();
return choiceArray[rnd.nextInt(choiceArray.length)];
}
}
형변환 오류를 위해 제네릭을 사용하면 아래와 같이 변경 가능하다.
public class Chooser<T> {
private final T[] choiceArray;
public Chooser(Collection<T> choices) {
// 오류 발생 incompatible types: java.lang.Object[] cannot be converted to T[]
// T[] 로 형변환 필요
// 하지만 여전히 Unchecked cast 경고는 발생
this.choiceArray = (T[]) choices.toArray();
}
public T choose() {
Random rnd = ThreadLocalRandom.current();
return choiceArray[rnd.nextInt(choiceArray.length)];
}
}
choose() 메소드 사용시 형변환에 대한 안전성은 확보 했지만, 생성자에서 Unchecked Cast 경고가 발생하고 있다. 타입 매개변수 T 가 어떤 타입인지 알 수 없기 때문에 형변환이 이뤄지는 런타임에 타입 안전성을 보장할 수 없다는 의미이다. (제네릭은 런타임에 타입 정보가 소거되기 때문) Unchecked Cast 경고는 배열을 리스트로 변경해주면 간단하게 해결 할 수 있다.
public class Chooser<T> {
private final List<T> choiceList;
public Chooser(Collection<T> choices) {
this.choiceList = new ArrayList<>(choices);
}
public T choose() {
Random rnd = ThreadLocalRandom.current();
return choiceList.get(rnd.nextInt(choiceList.size()));
}
}
배열 대신 리스트를 사용함으로써 런타임에 ClassCastException 을 만날일이 없어져서 타입 안전성이 보장된다.
종류
특징
배열
- 공변 - 실체화 - 런타임시 타입 안전
제네릭
- 불공변 - 타입 정보 소거 - 컴파일 타임 타입 안전
컴파일 타임에 타입 검증을 수행하는 제네릭을 사용해서 런타임에 타입 예외를 차단 할 수 있기 때문에 약간의 성능 감소가 있더라도 배열보다는 리스트를 사용하자.
배열과 리스트 차이
배열은 공변, 리스트는 불공변
배열은 공변이기 때문에 아래와 같은 배열 초기화는 가능하다. 배열은
Long
이Object
의 하위 타입이기 때문에Long[]
도Object[]
의 하위 타입임이 성립된다.반면 리스트는 불공변 이기 때문에 아래와 같은 리스트 초기화가는 불가하다. 리스트는
Long
이Object
의 하위 타입이라도List<Long>
과List<Object>
는 상위 혹은 하위 타입의 관계로 바라볼 수 없다.배열, 리스트 모두
Long
타입으로 초기화한 저장소에String
과 같은 다른 타입을 넣을 수 없다는 점은 동일하지만, 타입 안정성 측면에서 배열은 런타임에 진입해야 알 수 있고, 리스트는 컴파일 타임에 바로 인지 할 수 있다.배열은 실체화 된다.
배열은 실체화(Reifiable)가 되는데(JLS 4.7), 앞서든 예시 처럼
Long
배열에String
원소를 넣으면ArrayStoreException
이 발생한다. 이는 런타임에도 담기로 한 원소의 타입을 인지하고 확인하기 때문이다.반면 리스트는 타입 정보가 런타임에는 소거된다. (참조1, 참조2) 원소 타입을 컴파일 터임에만 검사하고, 런타임에는 알수 없다는 의미이다. 이는 제네릭 전 레거시 코드와 제네릭 타입을 함께 사용할 수 있게 해주는 메커니즘으로,
Java 5
에서 이를 통해 순조로운 전환을 할 수 있었다.제네릭 배열은 생성할 수 없다.
위와 같은 이유들로 인해 제네릭배열(
new List<E>[]
,new List<String>[]
,new E[]
)은 타입 안전하지 않기 때문에 생성할 수 없는데, 이는 아래와 같은 경우가 발생할 수 있기 때문이다.컴파일러가 자동 생성한 형변환 코드에서 런타임 시점에
ClassCastException
이 발생할 수 있기 때문이다. 이는ClassCastException
발생을 막겠다는 제네릭 타입의 취지와도 어긋나는 일이기도 하다. 이러한 이유로(1)
시점에서 부터 컴파일 오류를 발생시킨다.실체화 불가 타입
배열은 실체화가 가능하지만, 제네릭은 특별한 경우를 제외하면 실체화 불가(Non-Reifiable Types)하다.
String[], Integer[], List<?>, Map<?, ?>
...E, List<E>, List<String>
, ...제네릭은 타입 소거로 인해 실체화 되지 않아서 런타임에는 컴파일타임보다 타입 정보를 적게 가지는 타입을 의미한다. 예외가 있다면 비한정 와일드 카드 타입(#26 ) 이다.
배열보다는 리스트를 사용하면 된다.
배열로 형변환할때 제네릭 배열 생성 오류나 비검사 형변환 경고가 뜨는 경우 대부분 배열인
E[]
대신,List<E>
를 사용하면 해결 가능하다.형변환 오류를 위해 제네릭을 사용하면 아래와 같이 변경 가능하다.
choose()
메소드 사용시 형변환에 대한 안전성은 확보 했지만, 생성자에서Unchecked Cast
경고가 발생하고 있다. 타입 매개변수T
가 어떤 타입인지 알 수 없기 때문에 형변환이 이뤄지는 런타임에 타입 안전성을 보장할 수 없다는 의미이다. (제네릭은 런타임에 타입 정보가 소거되기 때문)Unchecked Cast
경고는 배열을 리스트로 변경해주면 간단하게 해결 할 수 있다.배열 대신 리스트를 사용함으로써 런타임에
ClassCastException
을 만날일이 없어져서 타입 안전성이 보장된다.- 실체화
- 런타임시 타입 안전
- 타입 정보 소거
- 컴파일 타임 타입 안전
컴파일 타임에 타입 검증을 수행하는 제네릭을 사용해서 런타임에 타입 예외를 차단 할 수 있기 때문에 약간의 성능 감소가 있더라도 배열보다는 리스트를 사용하자.