배열의 인덱스를 얻기 위해 ordinal을 쓰는 것은 일반적으로 좋지 않으니, 대신 EnumMap을 사용하라. 다차원 관계는 EnumMap<.., EnumMap<…>>으로 표현하라. “애플리케이션 프로그래머는 Enum.ordinal을 (웬만해서는) 사용하지 말아야 한다(아이템 35)”는 일반 원칙의 특수한 사례다.
좋지 않은 사례 1 : 배열이나 리스트에서 원소를 꺼낼 때, ordinal 메서드로 인덱스를 얻는 코드 더 좋은 해결책 EnumMap을 사용스트림을 이용해 맵을 관리하면 코드를 더 줄일 수 있음좋지 않은 사례 2: 두 열거 타입의 값들을 매핑하느라 ordinal을 (두 번이나) 쓴 배열들의 배열EnumMap 사용
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; } }
좋지 않은 사례 1 : 배열이나 리스트에서 원소를 꺼낼 때, ordinal 메서드로 인덱스를 얻는 코드
public static void main(String[] args) { Plant[] garden = { new Plant("바질", LifeCycle.ANNUAL), new Plant("캐러웨이", LifeCycle.BIENNIAL), new Plant("딜", LifeCycle.ANNUAL), new Plant("라벤더", LifeCycle.PERENNIAL), new Plant("파슬리", LifeCycle.BIENNIAL), new Plant("로즈마리", LifeCycle.PERENNIAL) }; // 코드 37-1 ordinal()을 배열 인덱스로 사용 - 따라 하지 말 것! (226쪽) 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]); }
- 동작은 하지만, 배열은 제네릭과 호환되지 않으니 비검사 형변환을 수행해야 함. 컴파일 깔끔하지 x
- 집합의 배열을 만들어서 해당 배열에 넣을 때 ordinal() 메서드를 사용을 함
- 배열은 각 인덱스의 의미를 모르니 출력 결과에 직접 레이블을 달아야 함.
- 가장 심각한 문제는 정확한 정숫값을 사용한다는 것을 사용자가 직접 보증해야 한다는 점임
- 정수는 열거 타입과 달리 타입 안전하지 않기 때문
더 좋은 해결책 EnumMap을 사용
- 열거 타입을 키로 사용할 수 있는 EnumMap을 사용하면 더 좋다
Map<Plant.LifeCycle, Set<Plant>> plantsByLifeCycle = new EnumMap<>(Plant.LifeCycle.class); // 인자로 Map의 키의 클래스를 넣음 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);
- 더 짧고 명료하고, 안전하고 성능도 원래 버전과 비등함
- EnumMap의 성능이 ordinal을 쓴 배열에 비견되는 이유는 그 내부에서 배열을 사용하기 때문임
- 내부 구현 방식을 안으로 숨겨서 Map의 타입 안전성과 배열의 성능을 모두 얻어냄
- 안전하지 않은 형변환은 쓰지 않고, 맵의 키인 열거 타입이 그 자체로 출력용 문자열을 제공하니 출력 결과에 직접 레이블을 달 필요도 없음
스트림을 이용해 맵을 관리하면 코드를 더 줄일 수 있음
// 코드 37-3 스트림을 사용한 코드 1 - EnumMap을 사용하지 않는다! (228쪽) System.out.println(Arrays.stream(garden) .collect(groupingBy(p -> p.lifeCycle))); // 코드 37-4 스트림을 사용한 코드 2 - EnumMap을 이용해 데이터와 열거 타입을 매핑했다. (228쪽) System.out.println(Arrays.stream(garden) .collect(groupingBy(p -> p.lifeCycle, () -> new EnumMap<>(LifeCycle.class), toSet())));
- 위의 코드는 EnumMap을 활용하지 않고 고유한 맵 구현체를 사용하기 때문에 EnumMap을 써서 얻은 공간과 성능 이점이 사라짐
- 아래 코드는 매개변수 3개짜리 Collectors.groupingBy 메서드를 이용하여 mapFactory 매개변수에 원하는 맵 구현체를 명시한 예임
- 스트림을 사용했을 때와 위의 for문을 사용했을 때의 차이는, 스트림을 사용하면 garden에 있는 lifeCycle에 대해서만 key가 만들어지고, for 문을 사용하면 모든 라이프 사이클에 대해 키가 만들어진다는 차이가 있음
좋지 않은 사례 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.ordianl()][to.ordinal()]; } } }
- 앞의 보여준 간단한 정원 예제와 마찬가지로 컴파일러는 ordinal과 배열 인덱스의 관계를 알 도리가 없음.
- 즉, Phase 나 Phase.Transition 열거 타입을 수정하면서 상전이 표 TRANSITIONS를 함께 수정하지 않거나 실수로 잘못 수정하면 런타임 오류가 발생함
- ArrayIndexOutOfBoundsException 이나 NullPointerException이 발생 가능함
- 상전이 표의 크기는 상태의 가짓수가 늘어나면 제곱해서 커지면 null로 채워지는 칸도 늘어남
EnumMap 사용
package effectivejava.chapter6.item37; import java.util.*; import java.util.stream.Stream; import static java.util.stream.Collectors.*; // 코드 37-6 중첩 EnumMap으로 데이터와 열거 타입 쌍을 연결했다. (229-231쪽) 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); // // 코드 37-7 EnumMap 버전에 새로운 상태 추가하기 (231쪽) // 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); 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); } } // 간단한 데모 프로그램 - 깔끔하지 못한 표를 출력한다. public static void main(String[] args) { for (Phase src : Phase.values()) { for (Phase dst : Phase.values()) { Transition transition = Transition.from(src, dst); if (transition != null) System.out.printf("%s에서 %s로 : %s %n", src, dst, transition); } } } }
- 전이 하나를 얻기 위해서 이전 상태(from)와 이후 상태(to) 가 필요하니, 맵 2개를 중첩하면 쉽게 해결이 가능함
- 바깥 맵은 이전 상태와 안쪽 맵을 연결하고, 안쪽 맵은 이후 상태와 전이를 연결
- 전이 전후의 두상태를 입력으로 받아서 Transition 인스턴스를 초기화 할 수 있음
- Map<Phase, Map<Phase, Transition>> 을 초기화하기 위해서 Collector를 두개를 사용하게 됨
- 첫 번째 groupingBy에서는 전이를 이전 상태를 기준으로 묶음
- 두 번째 toMap에서는 이후 상태를 키로 하는 맵을 만들어 전이에 대응시키는 EnumMap을 생성함
- 이렇게 만들면, Phase에 상수가 추가되고 Transition에 상수가 추가되기만 하면 기존 로직의 다른 부분은 수정할 필요가 없게 된다
- 배열로 만든 코드는 상수를 추가하고나서 배열들의 배열을 원소 9개짜리에서 16개짜리로 수정해야 하게 된다. 잘못된 순서로 입력하거나 너무 적거나 많이 기입해도 문제가 생기게 된다