이따금씩 배열이나 리스트에서 원소를 꺼낼 때 ordinal 메서드로 인덱스를 얻는 코드가 있다.
Plant 예제를 보자. 식물은 생애주기(LifeCycle)을 가지며, ANNUAL(한해살이), PERENNIAL(여러해살이), BIENNIAL(두해살이)가 있다.
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;
}
// 코드 37-1 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);
// 결과 출력
for (int i = 0; i < plantsByLifeCycleArr.length; i++) {
System.out.printf("%s: %s%n", Plant.LifeCycle.values()[i], plantsByLifeCycleArr[i]);
}
위의 코드는 동작은 하지만 문제가 많다.
비검사 형변환
을 수행해야 하고, 깔끔히 컴파일되지 않는다.ArrayIndexOutOfBoundException
을 던질 것이다.💡 EnumMap
열거 타입을 키로 사용하도록 설계한 아주 빠른 Map 구현체다.
위에서 배열은 실질적으로 열거 타입 상수 값으로 매핑하는 일을 했으니 EnumMap
으로 대체할 수 있으며, 위에서의 문제를 모두 해결할 수 있다.
// 코드 37-2 EnumMap을 사용해 데이터와 열거 타입을 매핑한다.
Map<Plant.LifeCycle, Set<Plant>> plantsByLifeCycle = new EnumMap<>(Plant.LifeCycle.class);
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);
스트림을 사용해 맵을 관리하면 코드를 더 줄일 수 있다.
// 코드 37-3 스트림을 사용한 코드 1 - EnumMap을 사용하지 않는다!
System.out.println(Arrays.stream(garden)
.collect(groupingBy(p -> p.lifeCycle)));
이 코드는 EnumMap을 사용하지 않고, 고유한 맵 구현체를 사용한다.
따라서 EnumMap의 공간과 성능의 이점이 사라진다는 문제가 있다.
매개변수 3개짜리 Collectors.groupingBy
메서드를 사용하면 된다.
두 번째 매개변수인 mapFactory에 원하는 맵 구현체를 명시해 호출할 수 있는데, 이 부분에 EnumMap을 사용하면 된다.
// 코드 37-4 스트림을 사용한 코드 2 - EnumMap을 이용해 데이터와 열거 타입을 매핑했다.
System.out.println(Arrays.stream(garden)
.collect(groupingBy(p -> p.lifeCycle,
() -> new EnumMap<>(LifeCycle.class), toSet())));
두 가지는 살짝 다르게 동작한다.
EnumMap 버전은 항상 식물의 생애주기당 중첩 맵을 하나씩 만든다. (즉, 모든 생애주기는 맵을 하나씩 가진다.)
반면에 스트림 버전은 해당 생애주기에 속하는 식물이 있을 때만 만든다.
예를 들어, 만약 한해살이와 여러해살이 식물만 있으면 두해살이 식물이 없다면
EnumMap 버전은 맵을 3개 만들지만, 스트림 버전은 2개만 만든다.
두 열거 타입 값들을 매핑할 때 ordinal을 두 번씩 쓴 배열들의 배열을 본 적이 있을 것이다.
두 가지 상태(Phase)를 전이(Transition)와 매핑하는 예제를 보자.
액체(LIQUID) → 고체(SOLID) : 응고(Freeze)
액체(LIQUID) → 기체(GAS) : 기화(BOIL)
public enum Phase {
SOLID, LIQUID, GAS;
public enum Transition {
MELT, FREEZE, BOIL, CONDENSE, SUBLIME, DEPOSIT;
// 행은 from의 ordinal을, 열은 to의 ordinal을 인덱스로 쓴다.
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()];
}
}
멋져보이지만, 이 코드에도 문제점이 많다.
// 코드 37-6 중첩 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>>
은 "이전 상태에서 '이후 상태에서 전이로의 맵'에 대응시키는 맵"이라는 뜻이다.
이러한 맵을 초기화하기 위해 Collector 2개를 차례로 사용했다.
groupingBy
: 전이를 이전 상태를 기준으로 묶는다.toMap
: 이후 상태를 전이에 대응시키는 EnumMap을 생성한다.여기에 새로운 상태인 플라즈마(PLASMA)를 추가해보자.
기체 → 플라즈마 : 이온화(IONIZE)
플라즈마 → 기체 : 탈이온화(DEIONIZE)
배열
에 플라즈마를 추가해야 한다면, 새로운 상수 Phase에 1개, Phase.Transition에 2갤르 추가하고, 원소 9개짜리인 배열들의 배열을 원소 16개짜리로 교체해야 한다.
반면에, EnumMap
버전에 추가할 때는 상태 목록에 플라즈마(PLASMA)를 추가하고, 전이 목록에 IONIZE(GAS, PLASMA)와 DEIONIZE(PLASMA, GAS)만 추가하면 된다.
// 코드 37-7 EnumMap 버전에 새로운 상태 추가하기
public enum Phase {
SOLID, LIQUID, GAS, PLASMA;
public enum Transition {
MELT(SOLID, LIQUID), FREEZE(LIQUID, SOLID),
BOIL(LIQUID, GAS), CONDENSE(GAS, LIQUID),
SUBLIME(SOLID, GAS), DEPOSIT(GAS, SOLID),
IONIZE(GAS, PLASMA), DEIONIZE(PLASMA, GAS);
...
}
}
배열의 인덱스를 얻기 위해
ordinal
을 쓰는 것은 일반적으로 좋지 않다. 대신EnumMap
을 사용하라.
다차원 관계는EnumMap<..., EnumMap<...>>
으로 표현하라.
"애플리케이션 프로그래머는 Enum.ordinal을 (웬만해서는) 사용하지 말아야 한다.(아이템 35)"는 일반 원칙의 특수한 사례다.