티스토리 뷰
배열과 제네릭 타입에는 중요한 차이가 두 가지 있습니다.
첫 번째, 배열은 공변(convariant)입니다.
어려워 보이는 단어지만 뜻은 간단합니다.
Sub가 Super의 하위 타입이라면 배열 Sub[]는 배열 Super[]의 하위 타입이 됩니다.
반면, 제네릭은 불공변(invariant)입니다.
즉, 서로 다른 타입 Type1과 Type2가 있을 때, List<Type1>은 List<Type2>의 하위 타입도 아니고 상위 타입도 아닙니다.
이것만 보면 제네릭에 문제가 있다고 생각할 수도 있지만, 사실 문제가 있는건 배열 쪽입니다.
다음은 문법상 허용되는 코드입니다.
Object[] objectArray = new Long[1];
objectArray[0] = "타입이 달라 넣을 수 없다."; // ArrayStoreException을 던진다.
하지만 다음 코드는 문법에 맞지 않습니다.
List<Object> ol = new ArrayList<Long>(); // 호환되지 않는 타입이다.
ol.add("타입이 달라 넣을 수 없다.");
어느 쪽이든 Long용 저장소에 String을 넣을 수는 없습니다.
다만 배열에서는 그 실수를 런타임에야 알게 되지만, 리스트를 사용하면 컴파일할 때 바로 알 수 있습니다.
두 번째, 배열은 실체화(reify)됩니다.
배열은 런타임에도 자신이 담기로 한 원소의 타입을 인지하고 확인합니다.
그렇기 때문에 Long 배열에 String을 넣으려 하면 ArrayStoreException이 발생합니다.
반면 앞서 이야기했듯 제네릭은 타입 정보가 런타임에는 소거됩니다.
원소 타입을 컴파일타임에만 검사하며 런타임에는 알 수 조차 없다는 뜻입니다.
소거는 제네릭이 지원되기 전의 레거시 코드와 제네릭 타입을 함께 사용할 수 있게 해주는 메커니즘으로, 자바 5가 제네릭으로 순조롭게 전환될 수 있도록 해줬습니다. (아이템 26)
이상의 주요 차이로 인해 배열과 제네릭은 잘 어우리지지 못합니다.
예컨대 배열은 제네릭 타입, 매개변수화 타입, 타입 매개변수로 사용할 수 없습니다.
즉, 코드를 new List<E>[], new List<String>[], new E[] 식으로 작성하면 컴파일할 때 제네릭 배열 생성 오류를 일으킵니다.
🤔
제네릭 배열을 만들지 못하게 막은 이유는 무엇일까요?
타입 안전하지 않기 때문입니다.
이를 허용한다면 컴파일러가 자동 생성한 형변환 코드에서 런타임에 ClassCastException이 발생할 수 있습니다.런타임에 ClassCastException이 발생하는 일을 막아주겠다는 제네릭 타입 시스템의 취지에 어긋납니다.
다음 코드로 구체적인 상황을 살펴봅시다.
List<String>[] stringLists = new List<String>[1]; // (1)
List<Integer> inList = 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)에서 컴파일 오류를 내야 합니다.
E, List<E>, List<String> 같은 타입을 실체화 불가 타입이라 합니다.
쉽게 말해, 실체화되지 않아서 런타임에는 컴파일타임보다 타입 정보를 적게 가지는 타입입니다.
소거 메커니즘 때문에 매개변수화 타입 가운데 실체화될 수 있는 타입은 List<?>와 Map<?,?> 같은 비한정적 와일드카드 타입뿐입니다.
배열을 비한정적 와일드카드 타입으로 만들 수는 있지만, 유용하게 쓰일 일은 거의 없습니다.
배열을 제네릭으로 만들 수 없어 귀찮을 때도 있습니다.
예컨대 제네릭 컬렉션에서는 자신의 원소 타입을 담은 배열을 반환하는 게 보통은 불가능합니다.
(완벽하지는 않지만 대부분의 상황에서 이 문제를 해결해주는 방법을 아이템 33에서 설명합니다.)
또한 제네릭 타입과 가변인수 메서드(varargs method, 아이템 53)를 함께 쓰면 해석하기 어려운 경고 메시지를 받게 됩니다.
가변인수 메서드를 호출할 때마다 가변인수 매개변수를 담을 배열이 하나 만들어지는데, 이 때 그 배열의 원소가 실체화 불가 타입이라면 경고가 발생하는 것입니다.
이 문제는 @SafeVarargs 애너테이션으로 대처할 수 있습니다. (아이템 32)
배열로 형변환할 때 제네릭 배열 생성 오류나 비검사 형변환 경고가 뜨는 경우 대부분은 배열인 E[] 대신 컬렉션인 List<E>를 사용하면 해결됩니다.
코드가 조금 복잡해지고 성능이 살짝 나빠질 수도 있지만, 그 대신 타입 안전성과 상호운용성은 좋아집니다.
생성자에서 컬렉션을 받는 Chooser 클래스를 예로 살펴봅시다.
이 클래스는 컬렉션 안의 원소 중 하나를 무작위로 선택해 반환하는 choose 메서드를 제공합니다.
생성자에 어떤 컬렉션을 넘기느냐에 따라 이 클래스를 주사위판, 매직 8볼, 몬테카를로 시뮬레이션용 데이터 소스 등으로 사용할 수 있습니다.
다음은 제네릭을 쓰지 않고 구현한 가장 간단한 버전입니다.
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를 원하는 타입으로 형변환해야 합니다.
혹시나 타입이 다른 원소가 들어 있었다면 런타임에 형변환 오류가 날 것입니다.
뒤에 나올 아이템 29의 조언을 가슴에 새기고 이 클래스를 제네릭으로 만들어봅시다.
public class Chooser<T> {
private final T[] choiceArray;
public Chooser(Collection<T> choices) {
choiceArray = choices.toArray();
}
public Object choose() {
Random rnd = ThreadLocalRandom.current();
return choiceArray[rnd.nextInt(choiceArray.length)];
}
}
이 클래스를 컴파일하면 다음의 오류 메시지가 출력될 것입니다.
Chooser.java:9: 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 배열로 형변환하면 됩니다.
choiceArray = (T[]) choices.toArray();
그런데 이번엔 경고가 뜹니다.
Chooser.java:9: warning: [unchecked] unchecked cast
choiceArray = choices.toArray();
^
required: T[], found: Object[]
where T is a type-variable:
T extends Obejct declared in class Chooser
T가 무슨 타입인지 알 수 없으니 컴파일러는 이 형변환이 런타임에도 안전한지 보장할 수 없다는 메시지입니다.
제네릭에서는 원소의 타입 장보가 소거되어 런타임에는 무슨 타입인지 알 수 없음을 기억합시다.
하지만 이 프로그램은 동작합니다. 단지 컴파일러가 안전을 보장하지 못할 뿐입니다.
코드를 작성하는 사람이 안전하다고 확신한다면 주석을 남기고 애너테이션을 달아 경고를 숨겨도 됩니다.
하지만 애초에 경고의 원인을 제거하는 편이 훨씬 낫습니다. (아이템 27)
비검사 형변환 경고를 제거하려면 배열 대신 리스트를 쓰면 됩니다.
다음 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()));
}
}
이번 버전은 코드양이 조금 늘었고 아마도 조금 더 느릴 테지만, 런타임에 ClassCastException을 만날 일은 없으니 그만한 가치가 있습니다.
정리
- 배열과 제네릭에는 매우 다른 타입 규칙이 적용된다.
- 배열은 공변이고 실체화되는 반면, 제네릭은 불공변이고 타입 정보가 소거된다.
- 그 결과 배열은 런타임에는 타입 안전하지만 컴파일타임에는 그렇지 않다.
- 제네릭은 그 반대다.
- 그래서 둘을 섞어 쓰기란 쉽지 않다.
- 둘을 섞어 쓰다가 컴파일 오류나 경고를 만나면, 가장 먼저 배열을 리스트로 대체해보자.
'JAVA > 이펙티브 자바' 카테고리의 다른 글
아이템[30]. 이왕이면 제너릭 메서드로 만들라 (0) | 2022.04.13 |
---|---|
아이템[29]. 이왕이면 제네릭 타입으로 만들라 (0) | 2022.04.13 |
아이템[27]. 비검사 경고를 제거하라 (0) | 2022.04.13 |
아이템[26]. 로 타입은 사용하지 말라 (0) | 2022.04.13 |
아이템[25]. 톱레벨 클래스는 한 파일에 하나만 담으라 (0) | 2022.04.10 |
- Total
- Today
- Yesterday
- 디자인패턴
- 이펙티브 자바
- 클린 코드
- C++
- 정규표현식
- 이팩티브 자바
- 백준
- kotest
- 클린 아키텍처
- Olympiad
- programmers
- AWS
- kkoon9
- Algorithm
- BOJ
- 코테
- node.js
- Effective Java
- BAEKJOON
- MSA
- 디자인 패턴
- Spring
- 객체지향
- Spring Boot
- JPA
- 프로그래머스
- 알고리즘
- 테라폼
- Kotlin
- Java
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | ||||||
2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 |