배열이나 리스트에서 원소를 꺼낼 때와 같이 인덱싱이 필요할때는, ordinal()
메서드(아이템 35)로 인덱스를 얻을 수 있다.
다음의 식물을 나타낸 Plant 클래스를 예로 들어보자.
class Plant {
enum LifeCycle { ANNUAL, PERENNIAL, BIENNIAL } // 생애주기
final String name;
final LifeCycle lifeCycle;
Plant(String name, LifeCycle lifeCycle) {
this.name = name;
this.lifeCycle = lifeCycle;
}
@Override public String toString() {
return name;
}
}
생애주기별로 총 3개의 집합을 만들고, 각 식물을 해당 집합에 넣자. 이때 주로 생애주기의 ordinal 값을 그 배열의 인덱스로 사용하려 할 것이다.
▶️ ordinal()을 배열 인덱스로 사용
Set<Plant>[] plantsByLifeCycleArr =
(Set<Plant>[]) new Set[Plant.LifeCycle.values().length];
for (int i = 0; i < plantsByLifeCycleArr.length; i++)
plantsByLifeCycleArr[i] = new HashSet<>();
for (Plant p : garden)
plantsByLifeCycleArr[p.lifeCycle.ordinal()].add(p); // ordinal()사용
// 결과 출력
for (int i = 0; i < plantsByLifeCycleArr.length; i++) {
System.out.printf("%s: %s%n",
Plant.LifeCycle.values()[i], plantsByLifeCycleArr[i]);
}
해당 코드는 동작은 하지만, 다음과 같은 문제가 존재한다.
1) 배열은 제네릭과 호환되지 않으니(아이템 28) 비검사 형변환을 수행해야 한다.
2) 정확한 정수값을 사용한다는 것을 직접 보증해야 한다. (정수는 타입 안전하지 않음)
👉 위 문제점의 해결책은, EnumMap을 사용하는 것이다.
EnumMap은 열거 타입을 키로 가지며, 배열과 마찬가지로 실질적으로 열거 타입 상수를 값으로 매핑하는 일을 한다.
참고로 키 타입의 Class 객체는 한정적 타입 토큰으로, 런타임 제네릭 타입 정보를 제공하고 있다.(아이템 33)
▶️ EnumMap을 사용해 데이터와 열거 타입을 매핑
Map<Plant.LifeCycle, Set<Plant>> plantsByLifeCycle =
new EnumMap<>(Plant.LifeCycle.class); // EnumMap
for (Plant.LifeCycle lc : Plant.LifeCycle.values())
plantsByLifeCycle.put(lc, new HashSet<>());
for (Plant p : garden)
plantsByLifeCycle.get(p.lifeCycle).add(p);
System.out.println(plantsByLifeCycle);
이전 버전보다 더 짧고 명료하고 안전하고 성능도 원래 버전과 동일하다. 자세히는 아래와 같은 장점을 가진다.
1) 안전하지 않은 형변환을 쓰지 않는다.
2) 맵의 키인 열거 타입이 그 자체로 출력용 문자열을 제공하니 출력 결과에 직접 레이블을 달 필요 없다.
3) EnumMap 내부 구현은 배열 방식이므로 성능이 동일하다.
스트림(아이템 45)를 사용해 맵을 관리하면 코드를 더 줄일 수 있다.
▶️ 스트림을 사용한 코드
Collectors.groupingBy()
메서드는 mapFactory 매개변수에 원하는 맵 구현체를 명시해 호출 할 수 있기 때문에, 다음과 같이 EnumMap을 이용해 데이터와 열거 타입을 매핑할 수 있다.
System.out.println(Arrays.stream(garden)
.collect(groupingBy(p -> p.lifeCycle,
() -> new EnumMap<>(LifeCycle.class), toSet())));
}
스트림 버전에서는 해당 생애주기에 속하는 식물이 있을 때만 만들고, EnumMap 버전은 언제나 생애주기당 하나씩 중첩 맵을 만든다는 점에서 차이가 존재한다.
두 열거 타입 값들을 매핑하느라, ordinal을 두 번씩이나 쓴 배열들의 배열의 예시를 보자. 다음은 이 방식을 이용해 두 가지 상태를 전이와 매핑하도록 구현한 코드이다.
▶️ 2중 배열의 인덱스에 ordinal() 사용
public enum Phase {
SOLID, LIQUID, GAS;
public enum Transition {
MELT, FREEZE, BOIL, CONDENSE, SUBLIME, DEPOSIT;
private static final Transition[][] TRANSITIONS = {
{null, MELT, SUBLIME },
{FREEZE, null, BOIL},
{DEPOSIT, CONDENSE, null}
};
// 한 상태에서 다른 상태로의 전이를 반환
public static Transition from(Phase from, Phase to){
return TRANSITIONS[from.ordinal()][to.ordinal()];
}
}
}
하지만, 마찬가지로 컴파일러는 ordinal과 배열 인덱스의 관계를 알 수 없다.
즉, Phase 나 Phase.Transition 열거 타입을 수정한다면 ArrayIndexOutOfBoundsException
이나 NullPointerException
예외를 던질 가능성이 높아지는 것이다.
전이 하나를 얻으려면 상태 2개가 필요하니, 맵 2개를 중첩하는 EnumMap을 사용하면 해당 문제를 쉽게 해결할 수 있다.(안쪽 맵은 이전 상태와 전이를 연결, 바깥 맵은 이후 상태와 안쪽 맵을 연결)
▶️ 중첩 EnumMap으로 데이터와 열거 타입 쌍을 연결
public enum Phase {
SOLID, LIQUID, GAS;
public enum Transition {
MELT(SOLID, LIQUID), FREEZE(LIQUID, SOLID),
BOIL(LIQUID, GAS), CONDENSE(GAS, LIQUID),
SUBLIME(SOLID, GAS), DEPOSIT(GAS, SOLID);
private final Phase from;
private final Phase to;
Transition(Phase from, Phase to) {
this.from = from;
this.to = to;
}
// 상전이 맵을 초기화
private static final Map<Phase, Map<Phase, Transition>>
m = Stream.of(values()).collect(groupingBy(t -> t.from,
() -> new EnumMap<>(Phase.class),
toMap(t -> t.to, t -> t,
(x, y) -> y, () -> new EnumMap<>(Phase.class))));
public static Transition from(Phase from, Phase to) {
return m.get(from).get(to);
}
}
}
Map<Phase, Map<Phase, Transition>>
는 "이전 상태에서 '이후 상태에서 전이로의 맵'에 대응시키는 맵" 이라는 뜻이다.
만약에 새로운 상태를 추가한다면, 배열로 만든 코드는 새로운 상수 추가 + 배열 크기 교체가 필요하지만 EnumMap 버전에서는 상태 목록(ex)PLASMA) 과 전이 목록(ex) IONIZE(GAS, PLASMA), DEIONIZE(PLASMA, GAS))에 원소만 추가해주면 끝이기 때문에 간단하다는 장점이 있다.
📚 핵심 정리
배열의 인덱스를 얻기 위해 ordinal을 쓰는 것은 일반적으로 좋지 않으니, 대신 EnumMap을 사용하라. 다차원 관계는EnumMap<..., EnumMap<...>>
으로 표현하라.