Item37 - ordinal 인덱싱 대신 EnumMap을 사용하라ordinal() 이란?예시코드 - 식물을 간단히 난타낸 클래스상황 예시코드 1 - ordinal 사용ordinal의 문제점?예시코드 2 - EnumMap 사용차이점예시코드 3 - Stream 활용두 열거 타입의 매핑 시 ordinal 사용의 문제점EnumMap으로 수정새로운 데이터 추가해보기결론
Item37 - ordinal 인덱싱 대신 EnumMap을 사용하라
ordinal() 이란?
- 해당 상수가 그 열거 타입에서 몇번째 위치에 있는지를 반환하는 메서드
예시코드 - 식물을 간단히 난타낸 클래스
public 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 class PlantManager { public static void main(String[] args) { Set<Plant>[] plantsByLifeCycle = (Set<Plant>[]) new Set[Plant.LifeCycle.values().length]; List<Plant> plants = Arrays.asList( new Plant("ANNUAL_TREE_1", LifeCycle.ANNUAL), new Plant("ANNUAL_TREE_2", LifeCycle.ANNUAL), new Plant("ANNUAL_TREE_3", LifeCycle.ANNUAL), new Plant("BIENNIAL_TREE_1", LifeCycle.BIENNIAL), new Plant("PERENNIAL_TREE_1", LifeCycle.PERENNIAL) ); for (int i = 0; i < plantsByLifeCycle.length; i++) { plantsByLifeCycle[i] = new HashSet<>(); } for (Plant p : plants) { plantsByLifeCycle[p.lifeCycle.ordinal()].add(p); } for (int i = 0; i < plantsByLifeCycle.length; i++) { System.out.printf("%s: %s%n", Plant.LifeCycle.values()[i], plantsByLifeCycle[i]); } } }

ordinal의 문제점?
- 배열은 제네릭과 호환되지 않기 때문에 비검사 형변환을 진행해야 하고 컴파일이 깔끔하게 진행되지 않는다.
- 배열은 각 인덱스의 의미를 모르기 때문에 출력 결과에 직접 레이블을 달아줘야 한다.
- 가장 심각한 문제로 정확한 정숫값을 사용해야 한다는 것을 사용자가 직접 보장해야 한다. 정수는 열거 타입과 달리 타입 안전하지 않기 때문이다.
- 잘못된 값을 사용하면 잘못된 동작을 수행하거나 ArrayIndexOutOfBoundsException을 던질 것이다.
예시코드 2 - EnumMap 사용
public class PlantManager { public static void main(String[] args) { Map<Plant.LifeCycle, Set<Plant>> plantsByLifeCycle = new EnumMap<>(Plant.LifeCycle.class); List<Plant> plants = Arrays.asList( new Plant("ANNUAL_TREE_1", LifeCycle.ANNUAL), new Plant("ANNUAL_TREE_2", LifeCycle.ANNUAL), new Plant("ANNUAL_TREE_3", LifeCycle.ANNUAL), new Plant("BIENNIAL_TREE_1", LifeCycle.BIENNIAL), new Plant("PERENNIAL_TREE_1", LifeCycle.PERENNIAL) ); for (Plant.LifeCycle lifeCycle : LifeCycle.values()) { plantsByLifeCycle.put(lifeCycle, new HashSet<>()); } for (Plant p : plants) { plantsByLifeCycle.get(p.lifeCycle).add(p); } System.out.println(plantsByLifeCycle); } }

차이점
- 코드가 더 간결해지고 성능도 비슷하다.
- 안전하지 않는 형변환은 쓰지 않고 맴의 키인 열거 타입이 그 자체로 출력용 문자열을 제공하니 출력 결과에 직접 레이블을 달아 줄 필요가 없다.
- 배열 인덱스를 계산하는 과정에서 오류가 날 일이 없으며 타입에 안전하게 사용이 가능하다.
예시코드 3 - Stream 활용
public class PlantManager { public static void main(String[] args) { List<Plant> plants = Arrays.asList( new Plant("ANNUAL_TREE_1", LifeCycle.ANNUAL), new Plant("ANNUAL_TREE_2", LifeCycle.ANNUAL), new Plant("ANNUAL_TREE_3", LifeCycle.ANNUAL), new Plant("BIENNIAL_TREE_1", LifeCycle.BIENNIAL), new Plant("PERENNIAL_TREE_1", LifeCycle.PERENNIAL) ); System.out.println( plants.stream() .collect( groupingBy(plant -> plant.lifeCycle) ) ); } }

- 이 코드는 EnumMap이 아닌 고유한 맵 구현체를 사용하기 때문에 EnumMap을 써서 얻은 공간과 성능 이점이 사라진다는 문제가 발생한다.
공간과 성능 이점이 사라진다?
Collectors의 groupingBy 메서드에 따로 사용할 Map의 구현체를 명시해주지 않으면 기본값으로 HashMap을 사용하게 되어 있어서 이 부분이 EnumMap을 사용했을때와 HashMap을 사용했을 때의 차이점을 알려주는 것 같다.
공간상의 이점으로는 HashMap의 경우 Value에 대해서 Node를 사용하고 있지만 EnumMap의 경우는 배열을 사용하고 있기 때문에 EnumMap이 공간상의 이점이 존재한다.
성능적인 면은 HashMap은 인덱스를 hash로 사용하여 key값을 해싱하는 과정이 추가적으로 들어가지만 EnumMap같은 경우는 열거타입의 상수 ordinal 값을 인덱스로 사용하기 때문에 성능면의 이점을 말해준 것 같다.
- 위 문제점을 최적화 하는 방법
- 매개변수 3개 짜리 Collections.groupingBy 메서드는 mapFactory 매개변수에 원하는 맵 구현체를 명시해 호출할 수 있다.
- 아래 코드는 Map을 빈번하게 사용하는 프로그램에서 최적화하는 방법이다.
public class PlantManager { public static void main(String[] args) { List<Plant> plants = Arrays.asList( new Plant("ANNUAL_TREE_1", LifeCycle.ANNUAL), new Plant("ANNUAL_TREE_2", LifeCycle.ANNUAL), new Plant("ANNUAL_TREE_3", LifeCycle.ANNUAL), new Plant("BIENNIAL_TREE_1", LifeCycle.BIENNIAL), new Plant("PERENNIAL_TREE_1", LifeCycle.PERENNIAL) ); System.out.println( plants.stream() .collect( groupingBy(plant -> plant.lifeCycle, () -> new EnumMap<>(LifeCycle.class), toSet() ) ) ); } }
- 스트림을 사용하면 EnumMap만 사용했을 때와는 다르게 동작한다.
- EnumMap 버전은 언제나 식물의 생애 주기당 하나씩의 중첩 맵을 만든다.
- Stream은 해당 생애주기에 존재하는 식물이 있을 때만 만들게 된다..
- EnumMap 버전에서는 Map을 3개 만들고, Stream 버전에서는 2개만 만든다.
- EnumMap의 경우
- Stream의 경우


두 열거 타입의 매핑 시 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을 사용 했을때와 마찬가지로 컴파일러는 ordinal과 배열 인덱스의 관계를 알 방법이 없다. 즉 Phase나 Phase.Transition 열거 타입을 수정하면 다시 배열의 데이터를 수정해야 하며 그렇지 않으면 잘못된 값이 나오거나 런타임 시 오류가 날 것이다.
- NPE, IndexOutOfMemory 등등
- 또한 데이터의 크기는 상태의 가짓수가 많아지면 제곱해서 커지게 되며 null도 그만큼 늘어나게 된다.
- 위의 예시 또한 ordinal을 사용하는 것 보다 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>>
- “이전 상태에서 ‘이후 상태에서 전이로의 맵’에 대응시키는 맵” 이라는 뜻이다.
- 이 맵을 초기화하기 위해 Collector를 2개를 차례대로 사용했다.
- 첫번째는 groupingBy에서 전이를 이전 상태를 기준으로 묶는다.
- 두번째는 toMap에서 이후 상태를 전이에 대응시키는 EnumMap을 생성한다.
- 병합 함수인 (x,y) → y는 선언만 하고 실제로 쓰진 않는다.
- EnumMap을 얻으려면 MapFactory가 필요하고 수집기(Collector)들은 점층적 팩터리를 제공하기 때문이다.
새로운 데이터 추가해보기
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); // 여기만 추가해주면 된다 ! 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()) // enum 타입 두 개를 매핑한 필드 리스트 .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); } } }
- 만약 ordinal을 사용했다면 배열의 원소들이 많아지고 그만큼 null도 많아졌을 것이다.
- EnumMap을 사용하면 상태목록만 추가해주면 된다.
- IONIZE(GAS, PLASMA), DEIONIZE(PLASMA, GAS);
- 나머지 코드는 그대로 보존되기 때문에 수정이 잘못 될 가능성히 아주 작다.
- 실제 내부에서는 맵들의 맵이 배열들의 배열로 구현되기 때문에 낭비되는 공간과 시간도 거의 없어 명확하고 안전하고 유지보수하기 좋다.
결론
- 배열의 인덱스를 얻기 위해서 ordinal을 쓰는 것은 일반적으로 좋지 않다.
- 위의 예시처럼 예외가 발생하거나 이상하게 동작할 수 있음
- ordinal을 사용하지 말고 EnumMap을 사용하자 !
- 코드가 간결해짐
- 배열 인덱스를 계산하는 과정에서 오류가 날 일이 없으며 타입에 안전하게 사용이 가능함
- …등등
- 다차원 관계는 EnumMap<K, EnumMap<K,V>>로 표현하도록 하자.