들어가며
배열이나 리스트에서 원소를 꺼낼 때 ordinal 메소드로 인덱스를 얻는 코드가 있다. 다음 코드를 보자.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 static void usingOrdinalAsArrayIndex () { Plant[] garden = getPlants(); Set<Plant>[] plantsByLifeCycle = (Set<Plant>[]) new Set [Plant.LifeCycle.values().length]; for (int i = 0 ; i < plantsByLifeCycle.length; i++) plantsByLifeCycle[i] = new HashSet <>(); for (Plant p : garden) plantsByLifeCycle[p.lifeCycle.ordinal()].add(p); for (Set<Plant> plants : plantsByLifeCycle) for (Plant plant : plants) System.out.println(plant.toString() + " " + plant.lifeCycle); } static Plant[] getPlants() { return new Plant [] { new Plant ("plant A" , Plant.LifeCycle.ANNUAL), new Plant ("plant B" , Plant.LifeCycle.ANNUAL), new Plant ("plant C" , Plant.LifeCycle.BIENNIAL), new Plant ("plant D" , Plant.LifeCycle.BIENNIAL), new Plant ("plant E" , Plant.LifeCycle.PERENNIAL), new Plant ("plant F" , Plant.LifeCycle.PERENNIAL)}; }
이는 동작은 하나 문제가 많다. 제네릭과 배열은 호환이 되지 않아 비검사 형변환을 해야하고 배열은 각 인덱스의 의미를 모르니 출력 결과에 직접 레이블을 달아야 한다. 또한 정확한 정숫값을 사용해야한다는 것을 사용자가 직접 보증해야 한다. 다음 코드를 보자.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 static void usingEnumMap () { Plant[] garden = getPlants(); 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); for (Plant plant : plantsByLifeCycle.get(Plant.LifeCycle.ANNUAL)) System.out.println(plant.toString() + " " + plant.lifeCycle); for (Plant plant : plantsByLifeCycle.get(Plant.LifeCycle.BIENNIAL)) System.out.println(plant.toString() + " " + plant.lifeCycle); for (Plant plant : plantsByLifeCycle.get(Plant.LifeCycle.PERENNIAL)) System.out.println(plant.toString() + " " + plant.lifeCycle); }
더 짧고 명료하고 안전하고 성능도 원래 것과 비등하다. 안전하지 않은 형변환은 쓰지 않고 맵의 키인 enum 타입이 그 자체로 출력용 문자열을 제공하니 직접 레이블을 달지 않아도 된다. 또한 배열 인덱스를 계산하는 과정에서 오류가 날 가능성도 봉쇄된다. EnumMap의 성능이 ordinal을 쓴 배열에 비견되는 이유는 그 내부에서 배열을 사용해 내부 구현을 안으로 숨겨 Map의 타입 안전성과 배열의 성능을 모두 얻어 내는데 있다. 또한 EnumMap의 생성자가 받는 키 타입의 Class 객체는 한정적 타입 토큰으로 런타임 제네릭 타입 정보를 제공한다.
1 2 3 4 5 6 7 8 9 10 11 public EnumMap (Class<K> keyType) { this .keyType = keyType; keyUniverse = getKeyUniverse(keyType); vals = new Object [keyUniverse.length]; }
스트림을 사용하면 코드를 더 줄일 수 있다. 다음 코드를 보자.
1 2 3 4 5 static void usingStreamMap () { Plant[] garden = getPlants(); System.out.println(Arrays.stream(garden).collect(groupingBy(p -> p.lifeCycle))); }
위 코드는 Map을 사용했기 때문에 EnumMap을 써서 얻은 공간과 성능 이점이 사라진다는 문제가 있다. 다음 코드를 보자.
1 2 3 4 5 6 7 static void usingStreamEnumMap () { Plant[] garden = getPlants(); System.out.println(Arrays.stream(garden).collect(groupingBy(p -> p.lifeCycle, () -> new EnumMap <>(Plant.LifeCycle.class), Collectors.toSet()))); }
위 코드를 통해, 매개변수 3개 짜리 groupingBy를 통해 mapFactory 매개변수에 원하는 맵 구현체를 명시할 수 있다. 또한 스트림을 사용하면 EnumMap만 사용했을 때와는 살짝 다르게 동작한다. EnumMap은 언제나 식물의 생애주기당 하나씩의 중첩 맵을 만들지만 스트림 버전에서는 해당 생애주기에 속하는 식물이 있을 때만 만든다.
1 2 3 4 5 6 7 8 ANNUAL PERENNIAL plant F PERENNIAL plant E PERENNIAL BIENNIAL plant D BIENNIAL plant C BIENNIAL {PERENNIAL=[plant F, plant E], BIENNIAL=[plant D, plant C]}
또한 두 열거 타입 값들을 매핑하느라 ordinal을 쓴 2D 배열을 본 적이 있을 것이다. 다음 코드가 그러하다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 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()]; } } }
이 또한 깨지기 쉬운 코드다. orinal과 배열 인덱스의 관계를 알 도리가 없고 열거 타입을 수정하면서 표의 관계도 함께 수정해줘야 한다. 또한 크기가 늘어마녀 제곱해서 커지며 null로 채워지는 칸도 늘어난다. 이제 다음 코드를 보자.
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 29 30 31 32 public enum Phase { SOLID, LIQUID, GAS; public enum Transition { MELT(SOLID, LIQUID), FREEZE(LIQUID, SOLID), BOIL(LIQUID, GAS), CONDENSE(GAS, SOLID), 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; } public 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); } } }
위 코드에서 첫번째 수집기에선 상태를 묶고 두번째 수집기에서는 이에 대응하는 EnumMap을 생성한다. 여기서 새로운 코드를 추가하는 것도 쉽다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public enum Phase { SOLID, LIQUID, GAS, PLASMA; public enum Transition { MELT(SOLID, LIQUID), FREEZE(LIQUID, SOLID), BOIL(LIQUID, GAS), CONDENSE(GAS, SOLID), SUBLIME(SOLID, GAS), DEPOSIT(GAS, SOLID), IONIZE(GAS, PLASMA), DEIONIZE(PLASMA, GAS); .... }
나머지는 기존 로직에서 잘 처리해준다. 배열의 인덱스를 얻기 위해 ordinal을 쓰는 것은 바람직하지 않으니 EnumMap을 사용하자.